Merge branch 'master' into xr-actions-D9124
This commit is contained in:
commit
e4e19a37d5
|
@ -30,7 +30,8 @@ bl_info = {
|
|||
|
||||
if "bpy" in locals():
|
||||
from importlib import reload
|
||||
#alphabetically sorted all add-on modules since reload only happens from __init__.
|
||||
|
||||
# alphabetically sorted all add-on modules since reload only happens from __init__.
|
||||
# modules with _bg are used for background computations in separate blender instance and that's why they don't need reload.
|
||||
|
||||
append_link = reload(append_link)
|
||||
|
@ -100,7 +101,6 @@ else:
|
|||
from blenderkit.bl_ui_widgets import bl_ui_draw_op
|
||||
# from blenderkit.bl_ui_widgets import bl_ui_textbox
|
||||
|
||||
|
||||
import os
|
||||
import math
|
||||
import time
|
||||
|
@ -110,7 +110,6 @@ import pathlib
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from bpy.app.handlers import persistent
|
||||
import bpy.utils.previews
|
||||
import mathutils
|
||||
|
@ -141,7 +140,6 @@ def scene_load(context):
|
|||
print('loading in background')
|
||||
print(bpy.context.window_manager)
|
||||
if not bpy.app.background:
|
||||
|
||||
search.load_previews()
|
||||
ui_props = bpy.context.scene.blenderkitUI
|
||||
ui_props.assetbar_on = False
|
||||
|
@ -164,7 +162,6 @@ def check_timers_timer():
|
|||
return 5.0
|
||||
|
||||
|
||||
|
||||
conditions = (
|
||||
('UNSPECIFIED', 'Unspecified', "Don't use this in search"),
|
||||
('NEW', 'New', 'Shiny new item'),
|
||||
|
@ -249,6 +246,7 @@ thumbnail_resolutions = (
|
|||
('2048', '2048', ''),
|
||||
)
|
||||
|
||||
|
||||
def udate_down_up(self, context):
|
||||
"""Perform a search if results are empty."""
|
||||
s = context.scene
|
||||
|
@ -257,6 +255,7 @@ def udate_down_up(self, context):
|
|||
if wm.get('search results') == None and props.down_up == 'SEARCH':
|
||||
search.search()
|
||||
|
||||
|
||||
def switch_search_results(self, context):
|
||||
s = bpy.context.scene
|
||||
wm = bpy.context.window_manager
|
||||
|
@ -279,7 +278,7 @@ def switch_search_results(self, context):
|
|||
elif props.asset_type == 'BRUSH':
|
||||
wm['search results'] = wm.get('bkit brush search')
|
||||
wm['search results orig'] = wm.get('bkit brush search orig')
|
||||
if not(context.sculpt_object or context.image_paint_object):
|
||||
if not (context.sculpt_object or context.image_paint_object):
|
||||
ui.add_report(
|
||||
'Switch to paint or sculpt mode to search in BlenderKit brushes.')
|
||||
|
||||
|
@ -288,7 +287,6 @@ def switch_search_results(self, context):
|
|||
search.search()
|
||||
|
||||
|
||||
|
||||
def asset_type_callback(self, context):
|
||||
'''
|
||||
Returns
|
||||
|
@ -303,7 +301,7 @@ def asset_type_callback(self, context):
|
|||
('MATERIAL', 'Materials', 'Find materials in the BlenderKit online database', 'MATERIAL', 2),
|
||||
# ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3),
|
||||
('SCENE', 'Scenes', 'Browse scenes', 'SCENE_DATA', 3),
|
||||
('HDR', 'Hdrs', 'Browse hdrs', 'WORLD', 4),
|
||||
('HDR', 'HDRs', 'Browse HDRs', 'WORLD', 4),
|
||||
('BRUSH', 'Brushes', 'Find brushes in the BlenderKit online database', 'BRUSH_DATA', 5)
|
||||
)
|
||||
else:
|
||||
|
@ -313,7 +311,7 @@ def asset_type_callback(self, context):
|
|||
('MATERIAL', 'Material', 'Upload a material to BlenderKit', 'MATERIAL', 2),
|
||||
# ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3),
|
||||
('SCENE', 'Scenes', 'Browse scenes', 'SCENE_DATA', 3),
|
||||
('HDR', 'Hdrs', 'Browse hdrs', 'WORLD', 4),
|
||||
('HDR', 'HDRs', 'Browse HDRs', 'WORLD', 4),
|
||||
('BRUSH', 'Brush', 'Upload a brush to BlenderKit', 'BRUSH_DATA', 5)
|
||||
)
|
||||
|
||||
|
@ -330,15 +328,17 @@ class BlenderKitUIProps(PropertyGroup):
|
|||
),
|
||||
description="BLenderKit",
|
||||
default="SEARCH",
|
||||
update = udate_down_up
|
||||
update=udate_down_up
|
||||
)
|
||||
asset_type: EnumProperty(
|
||||
name="BlenderKit Active Asset Type",
|
||||
items=asset_type_callback,
|
||||
description="Activate asset in UI",
|
||||
description="",
|
||||
default=None,
|
||||
update=switch_search_results
|
||||
)
|
||||
|
||||
asset_type_expand: BoolProperty(name="Expand asset types", default=False)
|
||||
# these aren't actually used ( by now, seems to better use globals in UI module:
|
||||
draw_tooltip: BoolProperty(name="Draw Tooltip", default=False)
|
||||
addon_update: BoolProperty(name="Should Update Addon", default=False)
|
||||
|
@ -533,7 +533,7 @@ class BlenderKitCommonSearchProps(object):
|
|||
('UPLOADING', 'Uploading', 'Uploading'),
|
||||
('UPLOADED', 'Uploaded', 'Uploaded'),
|
||||
('READY', 'Ready for V.', 'Ready for validation (deprecated since 2.8)'),
|
||||
('VALIDATED', 'Validated', 'Calidated'),
|
||||
('VALIDATED', 'Validated', 'Validated'),
|
||||
('ON_HOLD', 'On Hold', 'On Hold'),
|
||||
('REJECTED', 'Rejected', 'Rejected'),
|
||||
('DELETED', 'Deleted', 'Deleted'),
|
||||
|
@ -577,16 +577,14 @@ def name_update(self, context):
|
|||
utils.name_update(self)
|
||||
|
||||
|
||||
|
||||
|
||||
def update_free(self, context):
|
||||
if self.is_free == False:
|
||||
self.is_free = True
|
||||
ui_panels.ui_message(title = "All BlenderKit materials are free",
|
||||
message = "Any material uploaded to BlenderKit is free." \
|
||||
" However, it can still earn money for the author," \
|
||||
" based on our fair share system. " \
|
||||
"Part of subscription is sent to artists based on usage by paying users.")
|
||||
if self.is_free == 'FULL':
|
||||
self.is_free = 'FREE'
|
||||
ui_panels.ui_message(title="All BlenderKit materials are free",
|
||||
message="Any material uploaded to BlenderKit is free." \
|
||||
" However, it can still earn money for the author," \
|
||||
" based on our fair share system. " \
|
||||
"Part of subscription is sent to artists based on usage by paying users.\n")
|
||||
|
||||
|
||||
class BlenderKitCommonUploadProps(object):
|
||||
|
@ -667,9 +665,15 @@ class BlenderKitCommonUploadProps(object):
|
|||
# "Private assets are limited by quota",
|
||||
# default=False)
|
||||
|
||||
is_free: BoolProperty(name="Free for Everyone",
|
||||
description="You consent you want to release this asset as free for everyone",
|
||||
default=False)
|
||||
is_free: EnumProperty(
|
||||
name="Thumbnail Style",
|
||||
items=(
|
||||
('FREE', 'Free', "You consent you want to release this asset as free for everyone."),
|
||||
('FULL', 'Full', "Your asset will be only available for subscribers")
|
||||
),
|
||||
description="Assets can be in Free or in Full plan. Also free assets generate credits.",
|
||||
default="FULL",
|
||||
)
|
||||
|
||||
uploading: BoolProperty(name="Uploading",
|
||||
description="True when background process is running",
|
||||
|
@ -686,7 +690,7 @@ class BlenderKitCommonUploadProps(object):
|
|||
thumbnail_generating_state: StringProperty(
|
||||
name="Thumbnail Generating State",
|
||||
description="bg process reports for thumbnail generation",
|
||||
default='Please add thumbnail(jpg, at least 512x512)')
|
||||
default='Please add thumbnail(jpg or png, at least 512x512)')
|
||||
|
||||
report: StringProperty(
|
||||
name="Missing Upload Properties",
|
||||
|
@ -832,10 +836,19 @@ class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
default="",
|
||||
)
|
||||
|
||||
is_free: BoolProperty(name="Free for Everyone",
|
||||
description="You consent you want to release this asset as free for everyone",
|
||||
default=True, update=update_free
|
||||
)
|
||||
is_free: EnumProperty(
|
||||
name="Thumbnail Style",
|
||||
items=(
|
||||
('FREE', 'Free', "You consent you want to release this asset as free for everyone."),
|
||||
('FULL', 'Full', "Your asset will be only available for subscribers.")
|
||||
),
|
||||
description="Assets can be in Free or in Full plan. Also free assets generate credits. \n"
|
||||
"All BlenderKit materials are free.",
|
||||
default="FREE",
|
||||
update=update_free
|
||||
)
|
||||
|
||||
|
||||
|
||||
uv: BoolProperty(name="Needs UV", description="needs an UV set", default=False)
|
||||
# printable_3d : BoolProperty( name = "3d printable", description = "can be 3d printed", default = False)
|
||||
|
@ -889,7 +902,9 @@ class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Path to the thumbnail - 512x512 .jpg image",
|
||||
description="Thumbnail path - 512x512 .jpg image, rendered with cycles. \n"
|
||||
"Only standard BlenderKit previews will be accepted.\n"
|
||||
"Only exception are special effects like fire or similar.",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
update=autothumb.update_upload_material_preview)
|
||||
|
@ -932,6 +947,7 @@ class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
texture_resolution_max: IntProperty(name="Texture Resolution Max", description="texture resolution maximum",
|
||||
default=0)
|
||||
|
||||
|
||||
class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
mode: EnumProperty(
|
||||
name="Mode",
|
||||
|
@ -1005,7 +1021,7 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
|
||||
manufacturer: StringProperty(
|
||||
name="Manufacturer",
|
||||
description="Manufacturer, company making a design peace or product. Not you",
|
||||
description="Manufacturer, company making a design piece or product. Not you",
|
||||
default="",
|
||||
)
|
||||
|
||||
|
@ -1029,7 +1045,9 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Path to the thumbnail - 512x512 .jpg image",
|
||||
description="Thumbnail path - 512x512 .jpg\n"
|
||||
"Rendered with cycles",
|
||||
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
update=autothumb.update_upload_model_preview)
|
||||
|
@ -1215,7 +1233,8 @@ class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
|||
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Path to the thumbnail - 512x512 .jpg image",
|
||||
description="Thumbnail path - 512x512 .jpg\n"
|
||||
"Rendered with cycles",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
update=autothumb.update_upload_scene_preview)
|
||||
|
@ -1336,7 +1355,6 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
|||
update=search.search_update
|
||||
)
|
||||
|
||||
|
||||
# CONDITION
|
||||
search_condition: EnumProperty(
|
||||
items=conditions,
|
||||
|
@ -1508,10 +1526,11 @@ class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
|||
default="APPEND"
|
||||
)
|
||||
switch_after_append: BoolProperty(
|
||||
name = 'Switch to scene after download',
|
||||
default = False
|
||||
name='Switch to scene after download',
|
||||
default=False
|
||||
)
|
||||
|
||||
|
||||
def fix_subdir(self, context):
|
||||
'''Fixes project subdicrectory settings if people input invalid path.'''
|
||||
|
||||
|
@ -1524,11 +1543,10 @@ def fix_subdir(self, context):
|
|||
if self.project_subdir != pp:
|
||||
self.project_subdir = pp
|
||||
|
||||
ui_panels.ui_message(title = "Fixed to relative path",
|
||||
message = "This path should be always realative.\n" \
|
||||
" It's a directory BlenderKit creates where your .blend is \n " \
|
||||
"and uses it for storing assets.")
|
||||
|
||||
ui_panels.ui_message(title="Fixed to relative path",
|
||||
message="This path should be always realative.\n" \
|
||||
" It's a directory BlenderKit creates where your .blend is \n " \
|
||||
"and uses it for storing assets.")
|
||||
|
||||
|
||||
class BlenderKitAddonPreferences(AddonPreferences):
|
||||
|
@ -1687,6 +1705,14 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
|||
default=False,
|
||||
update=utils.save_prefs
|
||||
)
|
||||
|
||||
categories_fix: BoolProperty(
|
||||
name="Enable category fixing mode",
|
||||
description="Enable category fixing mode.",
|
||||
default=False,
|
||||
update=utils.save_prefs
|
||||
)
|
||||
|
||||
# allow_proximity : BoolProperty(
|
||||
# name="allow proximity data reports",
|
||||
# description="This sends anonymized proximity data \n \
|
||||
|
@ -1728,6 +1754,7 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
|||
if bpy.context.preferences.view.show_developer_ui:
|
||||
layout.prop(self, "use_timers")
|
||||
layout.prop(self, "experimental_features")
|
||||
layout.prop(self, "categories_fix")
|
||||
|
||||
|
||||
# registration
|
||||
|
|
|
@ -1267,6 +1267,7 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
|||
description='Replace resolution'
|
||||
)
|
||||
|
||||
#needs to be passed to the operator to not show all resolution possibilities
|
||||
max_resolution: IntProperty(
|
||||
name="Max resolution",
|
||||
description="",
|
||||
|
@ -1382,13 +1383,14 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
|||
# only make a pop up in case of switching resolutions
|
||||
if self.invoke_resolution:
|
||||
# show_enum_values(self, 'resolution')
|
||||
# print('ENUM VALUES')
|
||||
self.asset_data = self.get_asset_data(context)
|
||||
sprops = utils.get_search_props()
|
||||
if int(sprops.resolution) <= int(self.max_resolution):
|
||||
|
||||
#set initial resolutions enum activation
|
||||
if sprops.resolution != 'ORIGINAL' and int(sprops.resolution) <= int(self.max_resolution):
|
||||
self.resolution = sprops.resolution
|
||||
elif int(self.max_resolution) > 0:
|
||||
self.resolution = self.max_resolution
|
||||
self.resolution = str(self.max_resolution)
|
||||
else:
|
||||
self.resolution = 'ORIGINAL'
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import bpy
|
||||
import numpy
|
||||
import os
|
||||
import time
|
||||
|
||||
def get_orig_render_settings():
|
||||
rs = bpy.context.scene.render
|
||||
|
@ -67,6 +67,7 @@ def set_colorspace(img, colorspace):
|
|||
print(f'Colorspace {colorspace} not found.')
|
||||
|
||||
def generate_hdr_thumbnail():
|
||||
import numpy
|
||||
scene = bpy.context.scene
|
||||
ui_props = scene.blenderkitUI
|
||||
hdr_image = ui_props.hdr_upload_image#bpy.data.images.get(ui_props.hdr_upload_image)
|
||||
|
@ -98,3 +99,391 @@ def generate_hdr_thumbnail():
|
|||
inew.scale(thumbnailWidth, thumbnailHeight)
|
||||
|
||||
img_save_as(inew, filepath=inew.filepath)
|
||||
|
||||
|
||||
def find_color_mode(image):
|
||||
if not isinstance(image, bpy.types.Image):
|
||||
raise(TypeError)
|
||||
else:
|
||||
depth_mapping = {
|
||||
8: 'BW',
|
||||
24: 'RGB',
|
||||
32: 'RGBA',#can also be bw.. but image.channels doesn't work.
|
||||
96: 'RGB',
|
||||
128: 'RGBA',
|
||||
}
|
||||
return depth_mapping.get(image.depth,'RGB')
|
||||
|
||||
def find_image_depth(image):
|
||||
if not isinstance(image, bpy.types.Image):
|
||||
raise(TypeError)
|
||||
else:
|
||||
depth_mapping = {
|
||||
8: '8',
|
||||
24: '8',
|
||||
32: '8',#can also be bw.. but image.channels doesn't work.
|
||||
96: '16',
|
||||
128: '16',
|
||||
}
|
||||
return depth_mapping.get(image.depth,'8')
|
||||
|
||||
def can_erase_alpha(na):
|
||||
alpha = na[3::4]
|
||||
alpha_sum = alpha.sum()
|
||||
if alpha_sum == alpha.size:
|
||||
print('image can have alpha erased')
|
||||
# print(alpha_sum, alpha.size)
|
||||
return alpha_sum == alpha.size
|
||||
|
||||
|
||||
def is_image_black(na):
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
b = na[2::4]
|
||||
|
||||
rgbsum = r.sum() + g.sum() + b.sum()
|
||||
|
||||
# print('rgb sum', rgbsum, r.sum(), g.sum(), b.sum())
|
||||
if rgbsum == 0:
|
||||
print('image can have alpha channel dropped')
|
||||
return rgbsum == 0
|
||||
|
||||
def is_image_bw(na):
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
b = na[2::4]
|
||||
|
||||
rg_equal = r == g
|
||||
gb_equal = g == b
|
||||
rgbequal = rg_equal.all() and gb_equal.all()
|
||||
if rgbequal:
|
||||
print('image is black and white, can have channels reduced')
|
||||
|
||||
return rgbequal
|
||||
|
||||
|
||||
def numpytoimage(a, iname, width=0, height=0, channels=3):
|
||||
t = time.time()
|
||||
foundimage = False
|
||||
|
||||
for image in bpy.data.images:
|
||||
|
||||
if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]:
|
||||
i = image
|
||||
foundimage = True
|
||||
if not foundimage:
|
||||
if channels == 4:
|
||||
bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0, 1), alpha=True,
|
||||
generated_type='BLANK', float=True)
|
||||
if channels == 3:
|
||||
bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0), alpha=False,
|
||||
generated_type='BLANK', float=True)
|
||||
|
||||
i = None
|
||||
|
||||
for image in bpy.data.images:
|
||||
# print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1])
|
||||
if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
|
||||
i = image
|
||||
if i is None:
|
||||
i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, tiled=False)
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# d = a.shape[0] * a.shape[1]
|
||||
# a = a.swapaxes(0, 1)
|
||||
# a = a.reshape(d)
|
||||
# a = a.repeat(channels)
|
||||
# a[3::4] = 1
|
||||
i.pixels.foreach_set(a) # this gives big speedup!
|
||||
print('\ntime ' + str(time.time() - t))
|
||||
return i
|
||||
|
||||
|
||||
def imagetonumpy_flat(i):
|
||||
t = time.time()
|
||||
|
||||
import numpy
|
||||
|
||||
width = i.size[0]
|
||||
height = i.size[1]
|
||||
# print(i.channels)
|
||||
|
||||
size = width * height * i.channels
|
||||
na = numpy.empty(size, numpy.float32)
|
||||
i.pixels.foreach_get(na)
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# na = na[::4]
|
||||
# na = na.reshape(height, width, i.channels)
|
||||
# na = na.swapaxnes(0, 1)
|
||||
|
||||
# print('\ntime of image to numpy ' + str(time.time() - t))
|
||||
return na
|
||||
|
||||
def imagetonumpy(i):
|
||||
t = time.time()
|
||||
|
||||
import numpy as np
|
||||
|
||||
width = i.size[0]
|
||||
height = i.size[1]
|
||||
# print(i.channels)
|
||||
|
||||
size = width * height * i.channels
|
||||
na = np.empty(size, np.float32)
|
||||
i.pixels.foreach_get(na)
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# na = na[::4]
|
||||
na = na.reshape(height, width, i.channels)
|
||||
na = na.swapaxes(0, 1)
|
||||
|
||||
# print('\ntime of image to numpy ' + str(time.time() - t))
|
||||
return na
|
||||
|
||||
|
||||
def downscale(i):
|
||||
minsize = 128
|
||||
|
||||
sx, sy = i.size[:]
|
||||
sx = round(sx / 2)
|
||||
sy = round(sy / 2)
|
||||
if sx > minsize and sy > minsize:
|
||||
i.scale(sx, sy)
|
||||
|
||||
|
||||
def get_rgb_mean(i):
|
||||
'''checks if normal map values are ok.'''
|
||||
import numpy
|
||||
|
||||
na = imagetonumpy_flat(i)
|
||||
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
b = na[2::4]
|
||||
|
||||
rmean = r.mean()
|
||||
gmean = g.mean()
|
||||
bmean = b.mean()
|
||||
|
||||
rmedian = numpy.median(r)
|
||||
gmedian = numpy.median(g)
|
||||
bmedian = numpy.median(b)
|
||||
|
||||
# return(rmedian,gmedian, bmedian)
|
||||
return (rmean, gmean, bmean)
|
||||
|
||||
def check_nmap_mean_ok(i):
|
||||
'''checks if normal map values are in standard range.'''
|
||||
|
||||
rmean,gmean,bmean = get_rgb_mean(i)
|
||||
|
||||
#we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
|
||||
nmap_ok = 0.45< rmean < 0.55 and .45 < gmean < .55
|
||||
|
||||
return nmap_ok
|
||||
|
||||
|
||||
def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
||||
'''
|
||||
checks if normal map is directX or OpenGL.
|
||||
Returns - String value - DirectX and OpenGL
|
||||
'''
|
||||
import numpy
|
||||
width = i.size[0]
|
||||
height = i.size[1]
|
||||
|
||||
|
||||
|
||||
rmean, gmean, bmean = get_rgb_mean(i)
|
||||
|
||||
na = imagetonumpy(i)
|
||||
|
||||
if mask:
|
||||
mask = imagetonumpy(mask)
|
||||
|
||||
red_x_comparison = numpy.zeros((width, height), numpy.float32)
|
||||
green_y_comparison = numpy.zeros((width, height), numpy.float32)
|
||||
|
||||
if generated_test_images:
|
||||
red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) #images for debugging purposes
|
||||
green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32)#images for debugging purposes
|
||||
|
||||
ogl = numpy.zeros((width, height), numpy.float32)
|
||||
dx = numpy.zeros((width, height), numpy.float32)
|
||||
|
||||
if generated_test_images:
|
||||
ogl_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
|
||||
dx_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
|
||||
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
#try to mask with UV mask image
|
||||
if mask is None or mask[x,y,3]>0:
|
||||
|
||||
last_height_x = ogl[max(x - 1, 0), min(y, height - 1)]
|
||||
last_height_y = ogl[max(x,0), min(y - 1,height-1)]
|
||||
|
||||
diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
|
||||
diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
|
||||
calc_height = (last_height_x + last_height_y) \
|
||||
- diff_x - diff_y
|
||||
calc_height = calc_height /2
|
||||
ogl[x, y] = calc_height
|
||||
if generated_test_images:
|
||||
rgb = calc_height *.1 +.5
|
||||
ogl_img[x,y] = [rgb,rgb,rgb,1]
|
||||
|
||||
# green channel
|
||||
last_height_x = dx[max(x - 1, 0), min(y, height - 1)]
|
||||
last_height_y = dx[max(x, 0), min(y - 1, height - 1)]
|
||||
|
||||
diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
|
||||
diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
|
||||
calc_height = (last_height_x + last_height_y) \
|
||||
- diff_x + diff_y
|
||||
calc_height = calc_height / 2
|
||||
dx[x, y] = calc_height
|
||||
if generated_test_images:
|
||||
rgb = calc_height * .1 + .5
|
||||
dx_img[x, y] = [rgb, rgb, rgb, 1]
|
||||
|
||||
|
||||
ogl_std = ogl.std()
|
||||
dx_std = dx.std()
|
||||
|
||||
# print(mean_ogl, mean_dx)
|
||||
# print(max_ogl, max_dx)
|
||||
print(ogl_std, dx_std)
|
||||
print(i.name)
|
||||
# if abs(mean_ogl) > abs(mean_dx):
|
||||
if abs(ogl_std) > abs(dx_std):
|
||||
print('this is probably a DirectX texture')
|
||||
else:
|
||||
print('this is probably an OpenGL texture')
|
||||
|
||||
|
||||
if generated_test_images:
|
||||
# red_x_comparison_img = red_x_comparison_img.swapaxes(0,1)
|
||||
# red_x_comparison_img = red_x_comparison_img.flatten()
|
||||
#
|
||||
# green_y_comparison_img = green_y_comparison_img.swapaxes(0,1)
|
||||
# green_y_comparison_img = green_y_comparison_img.flatten()
|
||||
#
|
||||
# numpytoimage(red_x_comparison_img, 'red_' + i.name, width=width, height=height, channels=1)
|
||||
# numpytoimage(green_y_comparison_img, 'green_' + i.name, width=width, height=height, channels=1)
|
||||
|
||||
ogl_img = ogl_img.swapaxes(0, 1)
|
||||
ogl_img = ogl_img.flatten()
|
||||
|
||||
dx_img = dx_img.swapaxes(0, 1)
|
||||
dx_img = dx_img.flatten()
|
||||
|
||||
numpytoimage(ogl_img, 'OpenGL', width=width, height=height, channels=1)
|
||||
numpytoimage(dx_img, 'DirectX', width=width, height=height, channels=1)
|
||||
|
||||
if abs(ogl_std) > abs(dx_std):
|
||||
return 'DirectX'
|
||||
return 'OpenGL'
|
||||
|
||||
def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
|
||||
'''checks the image and saves it to drive with possibly reduced channels.
|
||||
Also can remove the image from the asset if the image is pure black
|
||||
- it finds it's usages and replaces the inputs where the image is used
|
||||
with zero/black color.
|
||||
currently implemented file type conversions:
|
||||
PNG->JPG
|
||||
'''
|
||||
colorspace = teximage.colorspace_settings.name
|
||||
teximage.colorspace_settings.name = 'Non-Color'
|
||||
#teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
|
||||
|
||||
JPEG_QUALITY = 90
|
||||
# is_image_black(na)
|
||||
# is_image_bw(na)
|
||||
|
||||
rs = bpy.context.scene.render
|
||||
ims = rs.image_settings
|
||||
|
||||
orig_file_format = ims.file_format
|
||||
orig_quality = ims.quality
|
||||
orig_color_mode = ims.color_mode
|
||||
orig_compression = ims.compression
|
||||
orig_depth = ims.color_depth
|
||||
|
||||
# if is_image_black(na):
|
||||
# # just erase the image from the asset here, no need to store black images.
|
||||
# pass;
|
||||
|
||||
# fp = teximage.filepath
|
||||
|
||||
# setup image depth, 8 or 16 bit.
|
||||
# this should normally divide depth with number of channels, but blender always states that number of channels is 4, even if there are only 3
|
||||
|
||||
print(teximage.name)
|
||||
print(teximage.depth)
|
||||
print(teximage.channels)
|
||||
|
||||
bpy.context.scene.display_settings.display_device = 'None'
|
||||
|
||||
image_depth = find_image_depth(teximage)
|
||||
|
||||
ims.color_mode = find_color_mode(teximage)
|
||||
#image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
|
||||
print('resulting depth set to:', image_depth)
|
||||
|
||||
fp = input_filepath
|
||||
if do_reductions:
|
||||
na = imagetonumpy_flat(teximage)
|
||||
|
||||
if can_erase_alpha(na):
|
||||
print(teximage.file_format)
|
||||
if teximage.file_format == 'PNG':
|
||||
print('changing type of image to JPG')
|
||||
base, ext = os.path.splitext(fp)
|
||||
teximage['original_extension'] = ext
|
||||
|
||||
fp = fp.replace('.png', '.jpg')
|
||||
fp = fp.replace('.PNG', '.jpg')
|
||||
|
||||
teximage.name = teximage.name.replace('.png', '.jpg')
|
||||
teximage.name = teximage.name.replace('.PNG', '.jpg')
|
||||
|
||||
teximage.file_format = 'JPEG'
|
||||
ims.quality = JPEG_QUALITY
|
||||
ims.color_mode = 'RGB'
|
||||
|
||||
if is_image_bw(na):
|
||||
ims.color_mode = 'BW'
|
||||
|
||||
ims.file_format = teximage.file_format
|
||||
ims.color_depth = image_depth
|
||||
|
||||
# all pngs with max compression
|
||||
if ims.file_format == 'PNG':
|
||||
ims.compression = 100
|
||||
# all jpgs brought to reasonable quality
|
||||
if ims.file_format == 'JPG':
|
||||
ims.quality = JPEG_QUALITY
|
||||
|
||||
if do_downscale:
|
||||
downscale(teximage)
|
||||
|
||||
|
||||
|
||||
# it's actually very important not to try to change the image filepath and packed file filepath before saving,
|
||||
# blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
|
||||
teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)
|
||||
if len(teximage.packed_files) > 0:
|
||||
teximage.unpack(method='REMOVE')
|
||||
teximage.filepath = fp
|
||||
teximage.filepath_raw = fp
|
||||
teximage.reload()
|
||||
|
||||
teximage.colorspace_settings.name = colorspace
|
||||
|
||||
ims.file_format = orig_file_format
|
||||
ims.quality = orig_quality
|
||||
ims.color_mode = orig_color_mode
|
||||
ims.compression = orig_compression
|
||||
ims.color_depth = orig_depth
|
|
@ -169,7 +169,7 @@ def slugify(slug):
|
|||
import unicodedata, re
|
||||
slug = slug.lower()
|
||||
|
||||
characters = '.," <>()'
|
||||
characters = '<>:"/\\|?*., ()'
|
||||
for ch in characters:
|
||||
slug = slug.replace(ch, '_')
|
||||
# import re
|
||||
|
@ -179,6 +179,8 @@ def slugify(slug):
|
|||
slug = re.sub(r'[-]+', '-', slug)
|
||||
slug = re.sub(r'/', '_', slug)
|
||||
slug = re.sub(r'\\\'\"', '_', slug)
|
||||
if len(slug)>50:
|
||||
slug = slug[:50]
|
||||
return slug
|
||||
|
||||
|
||||
|
|
|
@ -295,7 +295,7 @@ def update_ratings_work_hours_ui_1_5(self, context):
|
|||
|
||||
|
||||
class FastRateMenu(Operator):
|
||||
"""Fast rating of the assets directly in the asset bar - without need to download assets"""
|
||||
"""Rating of the assets , also directly from the asset bar - without need to download assets"""
|
||||
bl_idname = "wm.blenderkit_menu_rating_upload"
|
||||
bl_label = "Rate asset"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
|
|
@ -22,7 +22,6 @@ from blenderkit import paths, append_link, bg_blender, utils, download, search,
|
|||
import sys, json, os, time
|
||||
import subprocess
|
||||
import tempfile
|
||||
import numpy as np
|
||||
import bpy
|
||||
import requests
|
||||
import math
|
||||
|
@ -56,98 +55,9 @@ def get_current_resolution():
|
|||
return actres
|
||||
|
||||
|
||||
def can_erase_alpha(na):
|
||||
alpha = na[3::4]
|
||||
alpha_sum = alpha.sum()
|
||||
if alpha_sum == alpha.size:
|
||||
print('image can have alpha erased')
|
||||
# print(alpha_sum, alpha.size)
|
||||
return alpha_sum == alpha.size
|
||||
|
||||
|
||||
def is_image_black(na):
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
b = na[2::4]
|
||||
|
||||
rgbsum = r.sum() + g.sum() + b.sum()
|
||||
|
||||
# print('rgb sum', rgbsum, r.sum(), g.sum(), b.sum())
|
||||
if rgbsum == 0:
|
||||
print('image can have alpha channel dropped')
|
||||
return rgbsum == 0
|
||||
|
||||
|
||||
def is_image_bw(na):
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
b = na[2::4]
|
||||
|
||||
rg_equal = r == g
|
||||
gb_equal = g == b
|
||||
rgbequal = rg_equal.all() and gb_equal.all()
|
||||
if rgbequal:
|
||||
print('image is black and white, can have channels reduced')
|
||||
|
||||
return rgbequal
|
||||
|
||||
|
||||
def numpytoimage(a, iname, width=0, height=0, channels=3):
|
||||
t = time.time()
|
||||
foundimage = False
|
||||
|
||||
for image in bpy.data.images:
|
||||
|
||||
if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]:
|
||||
i = image
|
||||
foundimage = True
|
||||
if not foundimage:
|
||||
if channels == 4:
|
||||
bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0, 1), alpha=True,
|
||||
generated_type='BLANK', float=True)
|
||||
if channels == 3:
|
||||
bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0), alpha=False,
|
||||
generated_type='BLANK', float=True)
|
||||
|
||||
for image in bpy.data.images:
|
||||
# print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1])
|
||||
if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
|
||||
i = image
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# d = a.shape[0] * a.shape[1]
|
||||
# a = a.swapaxes(0, 1)
|
||||
# a = a.reshape(d)
|
||||
# a = a.repeat(channels)
|
||||
# a[3::4] = 1
|
||||
i.pixels.foreach_set(a) # this gives big speedup!
|
||||
print('\ntime ' + str(time.time() - t))
|
||||
return i
|
||||
|
||||
|
||||
def imagetonumpy(i):
|
||||
t = time.time()
|
||||
|
||||
width = i.size[0]
|
||||
height = i.size[1]
|
||||
# print(i.channels)
|
||||
|
||||
size = width * height * i.channels
|
||||
na = np.empty(size, np.float32)
|
||||
i.pixels.foreach_get(na)
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# na = na[::4]
|
||||
# na = na.reshape(height, width, i.channels)
|
||||
# na = na.swapaxnes(0, 1)
|
||||
|
||||
# print('\ntime of image to numpy ' + str(time.time() - t))
|
||||
return na
|
||||
|
||||
|
||||
def save_image_safely(teximage, filepath):
|
||||
'''
|
||||
Blender makes it really hard to save images... this is to fix it's crazy bad image saving.
|
||||
Blender makes it really hard to save images...
|
||||
Would be worth investigating PIL or similar instead
|
||||
Parameters
|
||||
----------
|
||||
|
@ -203,95 +113,8 @@ def extxchange_to_resolution(filepath):
|
|||
ext = 'jpg'
|
||||
|
||||
|
||||
def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
|
||||
'''checks the image and saves it to drive with possibly reduced channels.
|
||||
Also can remove the image from the asset if the image is pure black
|
||||
- it finds it's usages and replaces the inputs where the image is used
|
||||
with zero/black color.
|
||||
currently implemented file type conversions:
|
||||
PNG->JPG
|
||||
'''
|
||||
colorspace = teximage.colorspace_settings.name
|
||||
teximage.colorspace_settings.name = 'Non-Color'
|
||||
|
||||
JPEG_QUALITY = 90
|
||||
# is_image_black(na)
|
||||
# is_image_bw(na)
|
||||
|
||||
rs = bpy.context.scene.render
|
||||
ims = rs.image_settings
|
||||
|
||||
orig_file_format = ims.file_format
|
||||
orig_quality = ims.quality
|
||||
orig_color_mode = ims.color_mode
|
||||
orig_compression = ims.compression
|
||||
|
||||
# if is_image_black(na):
|
||||
# # just erase the image from the asset here, no need to store black images.
|
||||
# pass;
|
||||
|
||||
# fp = teximage.filepath
|
||||
fp = input_filepath
|
||||
if do_reductions:
|
||||
na = imagetonumpy(teximage)
|
||||
|
||||
if can_erase_alpha(na):
|
||||
print(teximage.file_format)
|
||||
if teximage.file_format == 'PNG':
|
||||
print('changing type of image to JPG')
|
||||
base, ext = os.path.splitext(fp)
|
||||
teximage['original_extension'] = ext
|
||||
|
||||
fp = fp.replace('.png', '.jpg')
|
||||
fp = fp.replace('.PNG', '.jpg')
|
||||
|
||||
teximage.name = teximage.name.replace('.png', '.jpg')
|
||||
teximage.name = teximage.name.replace('.PNG', '.jpg')
|
||||
|
||||
teximage.file_format = 'JPEG'
|
||||
ims.quality = JPEG_QUALITY
|
||||
ims.color_mode = 'RGB'
|
||||
|
||||
if is_image_bw(na):
|
||||
ims.color_mode = 'BW'
|
||||
|
||||
ims.file_format = teximage.file_format
|
||||
|
||||
# all pngs with max compression
|
||||
if ims.file_format == 'PNG':
|
||||
ims.compression = 100
|
||||
# all jpgs brought to reasonable quality
|
||||
if ims.file_format == 'JPG':
|
||||
ims.quality = JPEG_QUALITY
|
||||
|
||||
if do_downscale:
|
||||
downscale(teximage)
|
||||
|
||||
# it's actually very important not to try to change the image filepath and packed file filepath before saving,
|
||||
# blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
|
||||
teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)
|
||||
if len(teximage.packed_files) > 0:
|
||||
teximage.unpack(method='REMOVE')
|
||||
teximage.filepath = fp
|
||||
teximage.filepath_raw = fp
|
||||
teximage.reload()
|
||||
|
||||
teximage.colorspace_settings.name = colorspace
|
||||
|
||||
ims.file_format = orig_file_format
|
||||
ims.quality = orig_quality
|
||||
ims.color_mode = orig_color_mode
|
||||
ims.compression = orig_compression
|
||||
|
||||
|
||||
def downscale(i):
|
||||
minsize = 128
|
||||
|
||||
sx, sy = i.size[:]
|
||||
sx = round(sx / 2)
|
||||
sy = round(sy / 2)
|
||||
if sx > minsize and sy > minsize:
|
||||
i.scale(sx, sy)
|
||||
|
||||
|
||||
def upload_resolutions(files, asset_data):
|
||||
|
@ -340,9 +163,10 @@ def unpack_asset(data):
|
|||
pf.filepath = fp # bpy.path.abspath(fp)
|
||||
image.filepath = fp # bpy.path.abspath(fp)
|
||||
image.filepath_raw = fp # bpy.path.abspath(fp)
|
||||
image.save()
|
||||
# image.save()
|
||||
if len(image.packed_files) > 0:
|
||||
image.unpack(method='REMOVE')
|
||||
# image.unpack(method='REMOVE')
|
||||
image.unpack(method='WRITE_ORIGINAL')
|
||||
|
||||
bpy.ops.wm.save_mainfile(compress=False)
|
||||
# now try to delete the .blend1 file
|
||||
|
@ -523,11 +347,11 @@ def generate_lower_resolutions(data):
|
|||
# first, let's link the image back to the original one.
|
||||
i['blenderkit_original_path'] = i.filepath
|
||||
# first round also makes reductions on the image, while keeping resolution
|
||||
make_possible_reductions_on_image(i, fp, do_reductions=True, do_downscale=False)
|
||||
image_utils.make_possible_reductions_on_image(i, fp, do_reductions=True, do_downscale=False)
|
||||
|
||||
else:
|
||||
# lower resolutions only downscale
|
||||
make_possible_reductions_on_image(i, fp, do_reductions=False, do_downscale=True)
|
||||
image_utils.make_possible_reductions_on_image(i, fp, do_reductions=False, do_downscale=True)
|
||||
|
||||
abspath = bpy.path.abspath(i.filepath)
|
||||
if os.path.exists(abspath):
|
||||
|
@ -555,7 +379,7 @@ def generate_lower_resolutions(data):
|
|||
else:
|
||||
p2res = rkeys[rkeys.index(p2res) - 1]
|
||||
print('uploading resolution files')
|
||||
upload_resolutions(files, data['asset_data'])
|
||||
#upload_resolutions(files, data['asset_data'])
|
||||
preferences = bpy.context.preferences.addons['blenderkit'].preferences
|
||||
patch_asset_empty(data['asset_data']['id'], preferences.api_key)
|
||||
return
|
||||
|
@ -665,41 +489,6 @@ def get_materials_for_validation(page_size=100, max_results=100000000):
|
|||
return filepath
|
||||
|
||||
|
||||
# This gets all assets in the database through the/assets endpoint. Currently not used, since we use elastic for everything.
|
||||
# def get_assets_list():
|
||||
# bpy.app.debug_value = 2
|
||||
#
|
||||
# results = []
|
||||
# preferences = bpy.context.preferences.addons['blenderkit'].preferences
|
||||
# url = paths.get_api_url() + 'assets/all'
|
||||
# i = 0
|
||||
# while url is not None:
|
||||
# headers = utils.get_headers(preferences.api_key)
|
||||
# print('fetching assets from assets endpoint')
|
||||
# print(url)
|
||||
# retries = 0
|
||||
# while retries < 3:
|
||||
# r = rerequests.get(url, headers=headers)
|
||||
#
|
||||
# try:
|
||||
# adata = r.json()
|
||||
# url = adata.get('next')
|
||||
# print(i)
|
||||
# i += 1
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
# print('failed to get next')
|
||||
# if retries == 2:
|
||||
# url = None
|
||||
# if adata.get('results') != None:
|
||||
# results.extend(adata['results'])
|
||||
# retries = 3
|
||||
# print(f'fetched page {i}')
|
||||
# retries += 1
|
||||
#
|
||||
# fpath = assets_db_path()
|
||||
# with open(fpath, 'w', encoding = 'utf-8') as s:
|
||||
# json.dump(results, s, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def load_assets_list(filepath):
|
||||
|
@ -757,6 +546,7 @@ def generate_resolution_thread(asset_data, api_key):
|
|||
'''
|
||||
|
||||
fpath = download_asset(asset_data, unpack=True, api_key=api_key)
|
||||
|
||||
if fpath:
|
||||
if asset_data['assetType'] != 'hdr':
|
||||
print('send to bg ', fpath)
|
||||
|
|
|
@ -238,6 +238,8 @@ def parse_result(r):
|
|||
# utils.p('asset with no files-size')
|
||||
asset_type = r['assetType']
|
||||
if len(r['files']) > 0:#TODO remove this condition so all assets are parsed.
|
||||
get_author(r)
|
||||
|
||||
r['available_resolutions'] = []
|
||||
allthumbs = []
|
||||
durl, tname, small_tname = '', '', ''
|
||||
|
@ -553,7 +555,7 @@ def writeblockm(tooltip, mdata, key='', pretext=None, width=40): # for longer t
|
|||
|
||||
|
||||
def fmt_length(prop):
|
||||
prop = str(round(prop, 2)) + 'm'
|
||||
prop = str(round(prop, 2))
|
||||
return prop
|
||||
|
||||
|
||||
|
@ -590,9 +592,9 @@ def generate_tooltip(mdata):
|
|||
for b in bools_data:
|
||||
if mdata.get(b) and mdata[b]:
|
||||
mdata['tags'].append(b)
|
||||
t = writeblockm(t, mparams, key='designer', pretext='designer', width=col_w)
|
||||
t = writeblockm(t, mparams, key='manufacturer', pretext='manufacturer', width=col_w)
|
||||
t = writeblockm(t, mparams, key='designCollection', pretext='design collection', width=col_w)
|
||||
t = writeblockm(t, mparams, key='designer', pretext='Designer', width=col_w)
|
||||
t = writeblockm(t, mparams, key='manufacturer', pretext='Manufacturer', width=col_w)
|
||||
t = writeblockm(t, mparams, key='designCollection', pretext='Design collection', width=col_w)
|
||||
|
||||
# t = writeblockm(t, mparams, key='engines', pretext='engine', width = col_w)
|
||||
# t = writeblockm(t, mparams, key='model_style', pretext='style', width = col_w)
|
||||
|
@ -601,21 +603,22 @@ def generate_tooltip(mdata):
|
|||
# t = writeblockm(t, mparams, key='condition', pretext='condition', width = col_w)
|
||||
# t = writeblockm(t, mparams, key='productionLevel', pretext='production level', width = col_w)
|
||||
if has(mdata, 'purePbr'):
|
||||
t = writeblockm(t, mparams, key='pbrType', pretext='pbr', width=col_w)
|
||||
t = writeblockm(t, mparams, key='pbrType', pretext='Pbr', width=col_w)
|
||||
|
||||
t = writeblockm(t, mparams, key='designYear', pretext='design year', width=col_w)
|
||||
t = writeblockm(t, mparams, key='designYear', pretext='Design year', width=col_w)
|
||||
|
||||
if has(mparams, 'dimensionX'):
|
||||
t += 'size: %s, %s, %s\n' % (fmt_length(mparams['dimensionX']),
|
||||
t += 'Size: %s x %s x %sm\n' % (fmt_length(mparams['dimensionX']),
|
||||
fmt_length(mparams['dimensionY']),
|
||||
fmt_length(mparams['dimensionZ']))
|
||||
if has(mparams, 'faceCount'):
|
||||
t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])
|
||||
t += 'Face count: %s\n' % (mparams['faceCount'])
|
||||
# t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])
|
||||
|
||||
# write files size - this doesn't reflect true file size, since files size is computed from all asset files, including resolutions.
|
||||
if mdata.get('filesSize'):
|
||||
fs = utils.files_size_to_text(mdata['filesSize'])
|
||||
t += f'files size: {fs}\n'
|
||||
# if mdata.get('filesSize'):
|
||||
# fs = utils.files_size_to_text(mdata['filesSize'])
|
||||
# t += f'files size: {fs}\n'
|
||||
|
||||
# t = writeblockm(t, mparams, key='meshPolyType', pretext='mesh type', width = col_w)
|
||||
# t = writeblockm(t, mparams, key='objectCount', pretext='nubmber of objects', width = col_w)
|
||||
|
@ -624,36 +627,44 @@ def generate_tooltip(mdata):
|
|||
# t = writeblockm(t, mparams, key='modifiers', width = col_w)
|
||||
# t = writeblockm(t, mparams, key='shaders', width = col_w)
|
||||
|
||||
if has(mparams, 'textureSizeMeters'):
|
||||
t += 'texture size: %s\n' % fmt_length(mparams['textureSizeMeters'])
|
||||
# if has(mparams, 'textureSizeMeters'):
|
||||
# t += 'Texture size: %s m\n' % fmt_length(mparams['textureSizeMeters'])
|
||||
|
||||
if has(mparams, 'textureResolutionMax') and mparams['textureResolutionMax'] > 0:
|
||||
if not mparams.get('textureResolutionMin'): # for HDR's
|
||||
t = writeblockm(t, mparams, key='textureResolutionMax', pretext='Resolution', width=col_w)
|
||||
elif mparams.get('textureResolutionMin') == mparams['textureResolutionMax']:
|
||||
t = writeblockm(t, mparams, key='textureResolutionMin', pretext='texture resolution', width=col_w)
|
||||
t = writeblockm(t, mparams, key='textureResolutionMin', pretext='Texture resolution', width=col_w)
|
||||
else:
|
||||
t += 'tex resolution: %i - %i\n' % (mparams.get('textureResolutionMin'), mparams['textureResolutionMax'])
|
||||
t += 'Tex resolution: %i - %i\n' % (mparams.get('textureResolutionMin'), mparams['textureResolutionMax'])
|
||||
|
||||
if has(mparams, 'thumbnailScale'):
|
||||
t = writeblockm(t, mparams, key='thumbnailScale', pretext='preview scale', width=col_w)
|
||||
t = writeblockm(t, mparams, key='thumbnailScale', pretext='Preview scale', width=col_w)
|
||||
|
||||
# t += 'uv: %s\n' % mdata['uv']
|
||||
# t += '\n'
|
||||
t = writeblockm(t, mdata, key='license', width=col_w)
|
||||
if mdata.get('license') == 'cc_zero':
|
||||
t+= 'license: CC Zero\n'
|
||||
else:
|
||||
t+= 'license: Royalty free\n'
|
||||
# t = writeblockm(t, mdata, key='license', width=col_w)
|
||||
|
||||
fs = mdata.get('files')
|
||||
|
||||
if utils.profile_is_validator():
|
||||
if fs:
|
||||
resolutions = 'resolutions:'
|
||||
if fs and len(fs) > 2:
|
||||
resolutions = 'Resolutions:'
|
||||
list.sort(fs, key=lambda f: f['fileType'])
|
||||
for f in fs:
|
||||
if f['fileType'].find('resolution') > -1:
|
||||
resolutions += f['fileType'][11:] + ' '
|
||||
resolutions += '\n'
|
||||
t += resolutions
|
||||
t += resolutions.replace('_', '.')
|
||||
|
||||
t = writeblockm(t, mdata, key='isFree', width=col_w)
|
||||
if mdata['isFree']:
|
||||
t += 'Free plan\n'
|
||||
else:
|
||||
t += 'Full plan\n'
|
||||
else:
|
||||
if fs:
|
||||
for f in fs:
|
||||
|
@ -670,7 +681,7 @@ def generate_tooltip(mdata):
|
|||
# if adata != None:
|
||||
# t += generate_author_textblock(adata)
|
||||
|
||||
# t += '\n'
|
||||
t += '\n'
|
||||
rc = mdata.get('ratingsCount')
|
||||
if rc:
|
||||
t+='\n'
|
||||
|
@ -890,16 +901,62 @@ def get_profile():
|
|||
thread.start()
|
||||
return a
|
||||
|
||||
def query_to_url(query = {}, params = {}):
|
||||
# build a new request
|
||||
url = paths.get_api_url() + 'search/'
|
||||
|
||||
# build request manually
|
||||
# TODO use real queries
|
||||
requeststring = '?query='
|
||||
#
|
||||
if query.get('query') not in ('', None):
|
||||
requeststring += query['query'].lower()
|
||||
for i, q in enumerate(query):
|
||||
if q != 'query':
|
||||
requeststring += '+'
|
||||
requeststring += q + ':' + str(query[q]).lower()
|
||||
|
||||
# result ordering: _score - relevance, score - BlenderKit score
|
||||
order = []
|
||||
if params['free_first']:
|
||||
order = ['-is_free', ]
|
||||
if query.get('query') is None and query.get('category_subtree') == None:
|
||||
# assumes no keywords and no category, thus an empty search that is triggered on start.
|
||||
# orders by last core file upload
|
||||
if query.get('verification_status') == 'uploaded':
|
||||
# for validators, sort uploaded from oldest
|
||||
order.append('created')
|
||||
else:
|
||||
order.append('-last_upload')
|
||||
elif query.get('author_id') is not None and utils.profile_is_validator():
|
||||
|
||||
order.append('-created')
|
||||
else:
|
||||
if query.get('category_subtree') is not None:
|
||||
order.append('-score,_score')
|
||||
else:
|
||||
order.append('_score')
|
||||
requeststring += '+order:' + ','.join(order)
|
||||
|
||||
requeststring += '&addon_version=%s' % params['addon_version']
|
||||
if params.get('scene_uuid') is not None:
|
||||
requeststring += '&scene_uuid=%s' % params['scene_uuid']
|
||||
# print('params', params)
|
||||
urlquery = url + requeststring
|
||||
return urlquery
|
||||
|
||||
class Searcher(threading.Thread):
|
||||
query = None
|
||||
|
||||
def __init__(self, query, params, orig_result):
|
||||
def __init__(self, query, params, orig_result, tempdir = '', headers = None, urlquery = ''):
|
||||
super(Searcher, self).__init__()
|
||||
self.query = query
|
||||
self.params = params
|
||||
self._stop_event = threading.Event()
|
||||
self.result = orig_result
|
||||
self.tempdir = tempdir
|
||||
self.headers = headers
|
||||
self.urlquery = urlquery
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
@ -907,52 +964,6 @@ class Searcher(threading.Thread):
|
|||
def stopped(self):
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def query_to_url(self):
|
||||
query = self.query
|
||||
params = self.params
|
||||
# build a new request
|
||||
url = paths.get_api_url() + 'search/'
|
||||
|
||||
# build request manually
|
||||
# TODO use real queries
|
||||
requeststring = '?query='
|
||||
#
|
||||
if query.get('query') not in ('', None):
|
||||
requeststring += query['query'].lower()
|
||||
for i, q in enumerate(query):
|
||||
if q != 'query':
|
||||
requeststring += '+'
|
||||
requeststring += q + ':' + str(query[q]).lower()
|
||||
|
||||
# result ordering: _score - relevance, score - BlenderKit score
|
||||
order = []
|
||||
if params['free_first']:
|
||||
order = ['-is_free', ]
|
||||
if query.get('query') is None and query.get('category_subtree') == None:
|
||||
# assumes no keywords and no category, thus an empty search that is triggered on start.
|
||||
# orders by last core file upload
|
||||
if query.get('verification_status') == 'uploaded':
|
||||
# for validators, sort uploaded from oldest
|
||||
order.append('created')
|
||||
else:
|
||||
order.append('-last_upload')
|
||||
elif query.get('author_id') is not None and utils.profile_is_validator():
|
||||
|
||||
order.append('-created')
|
||||
else:
|
||||
if query.get('category_subtree') is not None:
|
||||
order.append('-score,_score')
|
||||
else:
|
||||
order.append('_score')
|
||||
requeststring += '+order:' + ','.join(order)
|
||||
|
||||
requeststring += '&addon_version=%s' % params['addon_version']
|
||||
if params.get('scene_uuid') is not None:
|
||||
requeststring += '&scene_uuid=%s' % params['scene_uuid']
|
||||
# print('params', params)
|
||||
urlquery = url + requeststring
|
||||
return urlquery
|
||||
|
||||
def run(self):
|
||||
maxthreads = 50
|
||||
query = self.query
|
||||
|
@ -961,22 +972,16 @@ class Searcher(threading.Thread):
|
|||
|
||||
t = time.time()
|
||||
mt('search thread started')
|
||||
tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
|
||||
# tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
|
||||
# json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type'])
|
||||
|
||||
headers = utils.get_headers(params['api_key'])
|
||||
|
||||
rdata = {}
|
||||
rdata['results'] = []
|
||||
|
||||
if params['get_next']:
|
||||
urlquery = self.result['next']
|
||||
if not params['get_next']:
|
||||
urlquery = self.query_to_url()
|
||||
|
||||
try:
|
||||
utils.p(urlquery)
|
||||
r = rerequests.get(urlquery, headers=headers) # , params = rparameters)
|
||||
utils.p(self.urlquery)
|
||||
r = rerequests.get(self.urlquery, headers=self.headers) # , params = rparameters)
|
||||
# print(r.url)
|
||||
reports = ''
|
||||
# utils.p(r.text)
|
||||
|
@ -1015,8 +1020,6 @@ class Searcher(threading.Thread):
|
|||
# END OF PARSING
|
||||
for d in rdata.get('results', []):
|
||||
|
||||
get_author(d)
|
||||
|
||||
for f in d['files']:
|
||||
# TODO move validation of published assets to server, too manmy checks here.
|
||||
if f['fileType'] == 'thumbnail' and f['fileThumbnail'] != None and f['fileThumbnailLarge'] != None:
|
||||
|
@ -1029,11 +1032,11 @@ class Searcher(threading.Thread):
|
|||
thumb_full_urls.append(f['fileThumbnailLarge'])
|
||||
|
||||
imgname = paths.extract_filename_from_url(f['fileThumbnail'])
|
||||
imgpath = os.path.join(tempdir, imgname)
|
||||
imgpath = os.path.join(self.tempdir, imgname)
|
||||
thumb_small_filepaths.append(imgpath)
|
||||
|
||||
imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
|
||||
imgpath = os.path.join(tempdir, imgname)
|
||||
imgpath = os.path.join(self.tempdir, imgname)
|
||||
thumb_full_filepaths.append(imgpath)
|
||||
|
||||
sml_thbs = zip(thumb_small_filepaths, thumb_small_urls)
|
||||
|
@ -1282,9 +1285,16 @@ def add_search_process(query, params, orig_result):
|
|||
old_thread = search_threads.pop(0)
|
||||
old_thread[0].stop()
|
||||
# TODO CARE HERE FOR ALSO KILLING THE Thumbnail THREADS.?
|
||||
# AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE
|
||||
# AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN NEWER ONE
|
||||
tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
|
||||
thread = Searcher(query, params, orig_result)
|
||||
headers = utils.get_headers(params['api_key'])
|
||||
|
||||
if params['get_next']:
|
||||
urlquery = orig_result['next']
|
||||
if not params['get_next']:
|
||||
urlquery = query_to_url(query, params)
|
||||
|
||||
thread = Searcher(query, params, orig_result, tempdir = tempdir, headers = headers, urlquery = urlquery)
|
||||
thread.start()
|
||||
|
||||
search_threads.append([thread, tempdir, query['asset_type'], {}]) # 4th field is for results
|
||||
|
@ -1394,7 +1404,7 @@ def search(category='', get_next=False, author_id=''):
|
|||
return;
|
||||
|
||||
if category != '':
|
||||
if utils.profile_is_validator():
|
||||
if utils.profile_is_validator() and user_preferences.categories_fix:
|
||||
query['category'] = category
|
||||
else:
|
||||
query['category_subtree'] = category
|
||||
|
|
|
@ -379,6 +379,7 @@ def draw_tooltip(x, y, text='', author='', img=None, gravatar=None):
|
|||
tsize = font_height
|
||||
else:
|
||||
fsize = font_height
|
||||
|
||||
if l[:4] == 'Tip:' or l[:11] == 'Please rate':
|
||||
tcol = textcol_strong
|
||||
fsize = font_height + 1
|
||||
|
@ -389,6 +390,7 @@ def draw_tooltip(x, y, text='', author='', img=None, gravatar=None):
|
|||
xtext += int(isizex / ncolumns)
|
||||
|
||||
column_lines = 1
|
||||
i=0
|
||||
for l in alines:
|
||||
if gravatar is not None:
|
||||
if column_lines == 1:
|
||||
|
@ -397,15 +399,20 @@ def draw_tooltip(x, y, text='', author='', img=None, gravatar=None):
|
|||
xtext -= gsize + textmargin
|
||||
|
||||
ytext = y - column_lines * line_height - nameline_height - ttipmargin - textmargin - isizey + texth
|
||||
if i == 0:
|
||||
if False:#i == 0:
|
||||
ytext = y - name_height + 5 - isizey + texth - textmargin
|
||||
elif i == len(lines) - 1:
|
||||
ytext = y - (nlines - 1) * line_height - nameline_height - ttipmargin * 2 - isizey + texth
|
||||
tcol = textcol
|
||||
tsize = font_height
|
||||
if (i> 0 and alines[i-1][:7] == 'Author:'):
|
||||
tcol = textcol_strong
|
||||
fsize = font_height + 2
|
||||
else:
|
||||
fsize = font_height
|
||||
if l[:4] == 'Tip:' or l[:11] == 'Please rate':
|
||||
tcol = textcol
|
||||
|
||||
if l[:4] == 'Tip:' or l[:11] == 'Please rate' :
|
||||
tcol = textcol_strong
|
||||
fsize = font_height + 1
|
||||
|
||||
|
@ -1486,7 +1493,7 @@ class AssetBarOperator(bpy.types.Operator):
|
|||
|
||||
if ui_props.drag_init:
|
||||
ui_props.drag_length += 1
|
||||
if ui_props.drag_length > 0:
|
||||
if ui_props.drag_length > 2:
|
||||
ui_props.dragging = True
|
||||
ui_props.drag_init = False
|
||||
|
||||
|
@ -1617,7 +1624,7 @@ class AssetBarOperator(bpy.types.Operator):
|
|||
return {'RUNNING_MODAL'}
|
||||
|
||||
# Drag-drop interaction
|
||||
if ui_props.dragging and mouse_in_region(r, mx, my):
|
||||
if ui_props.dragging and mouse_in_region(r, mx, my):# and ui_props.drag_length>10:
|
||||
asset_search_index = ui_props.active_index
|
||||
# raycast here
|
||||
ui_props.active_index = -3
|
||||
|
|
|
@ -143,14 +143,22 @@ def draw_upload_common(layout, props, asset_type, context):
|
|||
layout.prop(props, 'is_private', expand=True)
|
||||
if props.is_private == 'PUBLIC':
|
||||
layout.prop(props, 'license')
|
||||
layout.prop(props, 'is_free', expand=True)
|
||||
|
||||
prop_needed(layout, props, 'name', props.name)
|
||||
if props.is_private == 'PUBLIC':
|
||||
prop_needed(layout, props, 'description', props.description)
|
||||
prop_needed(layout, props, 'tags', props.tags)
|
||||
else:
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'tags')
|
||||
|
||||
def poll_local_panels():
|
||||
user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
|
||||
return user_preferences.panel_behaviour == 'BOTH' or user_preferences.panel_behaviour == 'LOCAL'
|
||||
|
||||
|
||||
def prop_needed(layout, props, name, value, is_not_filled=''):
|
||||
def prop_needed(layout, props, name, value='', is_not_filled=''):
|
||||
row = layout.row()
|
||||
if value == is_not_filled:
|
||||
# row.label(text='', icon = 'ERROR')
|
||||
|
@ -180,9 +188,6 @@ def draw_panel_hdr_upload(self, context):
|
|||
|
||||
draw_upload_common(layout, props, 'HDR', context)
|
||||
|
||||
layout.prop(props, 'name')
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'tags')
|
||||
|
||||
|
||||
def draw_panel_hdr_search(self, context):
|
||||
|
@ -208,12 +213,10 @@ def draw_panel_model_upload(self, context):
|
|||
|
||||
draw_upload_common(layout, props, 'MODEL', context)
|
||||
|
||||
prop_needed(layout, props, 'name', props.name)
|
||||
|
||||
col = layout.column()
|
||||
if props.is_generating_thumbnail:
|
||||
col.enabled = False
|
||||
prop_needed(col, props, 'thumbnail', props.has_thumbnail, False)
|
||||
prop_needed(col, props, 'thumbnail', props.thumbnail)
|
||||
if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'):
|
||||
col.operator("object.blenderkit_generate_thumbnail", text='Generate thumbnail', icon='IMAGE')
|
||||
|
||||
|
@ -227,15 +230,12 @@ def draw_panel_model_upload(self, context):
|
|||
elif props.thumbnail_generating_state != '':
|
||||
utils.label_multiline(layout, text=props.thumbnail_generating_state)
|
||||
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'tags')
|
||||
# prop_needed(layout, props, 'style', props.style)
|
||||
# prop_needed(layout, props, 'production_level', props.production_level)
|
||||
layout.prop(props, 'style')
|
||||
layout.prop(props, 'production_level')
|
||||
|
||||
layout.prop(props, 'condition')
|
||||
layout.prop(props, 'is_free')
|
||||
layout.prop(props, 'pbr')
|
||||
layout.label(text='design props:')
|
||||
layout.prop(props, 'manufacturer')
|
||||
|
@ -272,8 +272,6 @@ def draw_panel_scene_upload(self, context):
|
|||
# else:
|
||||
# layout.operator("object.blenderkit_auto_tags", text='Auto fill tags')
|
||||
|
||||
prop_needed(layout, props, 'name', props.name)
|
||||
|
||||
col = layout.column()
|
||||
# if props.is_generating_thumbnail:
|
||||
# col.enabled = False
|
||||
|
@ -291,9 +289,8 @@ def draw_panel_scene_upload(self, context):
|
|||
# elif props.thumbnail_generating_state != '':
|
||||
# utils.label_multiline(layout, text = props.thumbnail_generating_state)
|
||||
|
||||
layout.prop(props, 'is_free')
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'tags')
|
||||
|
||||
|
||||
layout.prop(props, 'style')
|
||||
layout.prop(props, 'production_level')
|
||||
layout.prop(props, 'use_design_year')
|
||||
|
@ -623,30 +620,16 @@ def draw_panel_material_upload(self, context):
|
|||
|
||||
draw_upload_common(layout, props, 'MATERIAL', context)
|
||||
|
||||
prop_needed(layout, props, 'name', props.name)
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'style')
|
||||
# if props.style == 'OTHER':
|
||||
# layout.prop(props, 'style_other')
|
||||
# layout.prop(props, 'engine')
|
||||
# if props.engine == 'OTHER':
|
||||
# layout.prop(props, 'engine_other')
|
||||
layout.prop(props, 'tags')
|
||||
# layout.prop(props,'shaders')#TODO autofill on upload
|
||||
# row = layout.row()
|
||||
layout.prop(props, 'is_free')
|
||||
|
||||
layout.prop(props, 'pbr')
|
||||
layout.prop(props, 'uv')
|
||||
layout.prop(props, 'animated')
|
||||
layout.prop(props, 'texture_size_meters')
|
||||
|
||||
# THUMBNAIL
|
||||
row = layout.row()
|
||||
if props.is_generating_thumbnail:
|
||||
row.enabled = False
|
||||
prop_needed(row, props, 'thumbnail', props.has_thumbnail, False)
|
||||
|
||||
|
||||
|
||||
if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'):
|
||||
layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT')
|
||||
if props.is_generating_thumbnail:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=props.thumbnail_generating_state, icon='RENDER_STILL')
|
||||
|
@ -656,8 +639,21 @@ def draw_panel_material_upload(self, context):
|
|||
elif props.thumbnail_generating_state != '':
|
||||
utils.label_multiline(layout, text=props.thumbnail_generating_state)
|
||||
|
||||
if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'):
|
||||
layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT')
|
||||
layout.prop(props, 'style')
|
||||
# if props.style == 'OTHER':
|
||||
# layout.prop(props, 'style_other')
|
||||
# layout.prop(props, 'engine')
|
||||
# if props.engine == 'OTHER':
|
||||
# layout.prop(props, 'engine_other')
|
||||
# layout.prop(props,'shaders')#TODO autofill on upload
|
||||
# row = layout.row()
|
||||
|
||||
layout.prop(props, 'pbr')
|
||||
layout.prop(props, 'uv')
|
||||
layout.prop(props, 'animated')
|
||||
layout.prop(props, 'texture_size_meters')
|
||||
|
||||
|
||||
|
||||
# tname = "." + bpy.context.active_object.active_material.name + "_thumbnail"
|
||||
# if props.has_thumbnail and bpy.data.textures.get(tname) is not None:
|
||||
|
@ -701,10 +697,6 @@ def draw_panel_brush_upload(self, context):
|
|||
|
||||
draw_upload_common(layout, props, 'BRUSH', context)
|
||||
|
||||
layout.prop(props, 'name')
|
||||
layout.prop(props, 'description')
|
||||
layout.prop(props, 'tags')
|
||||
|
||||
|
||||
def draw_panel_brush_search(self, context):
|
||||
s = context.scene
|
||||
|
@ -974,8 +966,20 @@ class VIEW3D_PT_blenderkit_unified(Panel):
|
|||
row.scale_x = 1.6
|
||||
row.scale_y = 1.6
|
||||
# split = row.split(factor=.
|
||||
col = layout.column()
|
||||
col.prop(ui_props, 'asset_type', expand=True, icon_only=False)
|
||||
|
||||
expand_icon = 'TRIA_DOWN'
|
||||
if ui_props.asset_type_expand:
|
||||
expand_icon = 'TRIA_RIGHT'
|
||||
row = layout.row()
|
||||
split = row.split(factor = 0.1)
|
||||
split.prop(ui_props, 'asset_type_expand', icon = expand_icon, icon_only = True, emboss = False)
|
||||
split = split.split(factor = 1.0)
|
||||
if ui_props.asset_type_expand:
|
||||
#expanded interface with names in column
|
||||
split = split.row()
|
||||
split.scale_x = 1.6
|
||||
split.scale_y = 1.6
|
||||
split.prop(ui_props, 'asset_type', expand=True, icon_only=ui_props.asset_type_expand)
|
||||
# row = layout.column(align = False)
|
||||
# layout.prop(ui_props, 'asset_type', expand=False, text='')
|
||||
|
||||
|
@ -1588,9 +1592,10 @@ def draw_panel_categories(self, context):
|
|||
# op.free_only = True
|
||||
|
||||
for c in cats['children']:
|
||||
if c['assetCount'] > 0 or utils.profile_is_validator():
|
||||
if c['assetCount'] > 0 or (utils.profile_is_validator() and user_preferences.categories_fix):
|
||||
row = col.row(align=True)
|
||||
if len(c['children']) > 0 and c['assetCount'] > 15 or utils.profile_is_validator():
|
||||
if len(c['children']) > 0 and c['assetCount'] > 15 or (
|
||||
utils.profile_is_validator() and user_preferences.categories_fix):
|
||||
row = row.split(factor=.8, align=True)
|
||||
# row = split.split()
|
||||
ctext = '%s (%i)' % (c['name'], c['assetCount'])
|
||||
|
@ -1603,8 +1608,8 @@ def draw_panel_categories(self, context):
|
|||
op.do_search = True
|
||||
op.keep_running = True
|
||||
op.category = c['slug']
|
||||
# TODO enable subcategories, now not working due to some bug on server probably
|
||||
if len(c['children']) > 0 and c['assetCount'] > 15 or utils.profile_is_validator():
|
||||
if len(c['children']) > 0 and c['assetCount'] > 15 or (
|
||||
utils.profile_is_validator() and user_preferences.categories_fix):
|
||||
# row = row.split()
|
||||
op = row.operator('view3d.blenderkit_set_category', text='>>')
|
||||
op.asset_type = ui_props.asset_type
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
|
||||
from blenderkit import asset_inspector, paths, utils, bg_blender, autothumb, version_checker, search, ui_panels, ui, \
|
||||
overrides, colors, rerequests, categories, upload_bg, tasks_queue, image_utils
|
||||
overrides, colors, rerequests, categories, upload_bg, tasks_queue, image_utils
|
||||
|
||||
import tempfile, os, subprocess, json, re
|
||||
|
||||
|
@ -47,6 +47,7 @@ licenses = (
|
|||
('cc_zero', 'Creative Commons Zero', 'Creative Commons Zero'),
|
||||
)
|
||||
|
||||
|
||||
def comma2array(text):
|
||||
commasep = text.split(',')
|
||||
ar = []
|
||||
|
@ -71,50 +72,32 @@ def add_version(data):
|
|||
|
||||
|
||||
def write_to_report(props, text):
|
||||
props.report = props.report + text + '\n'
|
||||
props.report = props.report + ' - '+ text + '\n\n'
|
||||
|
||||
|
||||
def check_missing_data_model(props):
|
||||
props.report = ''
|
||||
autothumb.update_upload_model_preview(None, None)
|
||||
|
||||
if props.name == '':
|
||||
write_to_report(props, 'Set model name')
|
||||
# if props.tags == '':
|
||||
# write_to_report(props, 'Write at least 3 tags')
|
||||
if not props.has_thumbnail:
|
||||
write_to_report(props, 'Add thumbnail:')
|
||||
|
||||
props.report += props.thumbnail_generating_state + '\n'
|
||||
if props.engine == 'NONE':
|
||||
write_to_report(props, 'Set at least one rendering/output engine')
|
||||
if not any(props.dimensions):
|
||||
write_to_report(props, 'Run autotags operator or fill in dimensions manually')
|
||||
|
||||
# if not any(props.dimensions):
|
||||
# write_to_report(props, 'Run autotags operator or fill in dimensions manually')
|
||||
|
||||
|
||||
def check_missing_data_scene(props):
|
||||
props.report = ''
|
||||
autothumb.update_upload_model_preview(None, None)
|
||||
|
||||
if props.name == '':
|
||||
write_to_report(props, 'Set scene name')
|
||||
# if props.tags == '':
|
||||
# write_to_report(props, 'Write at least 3 tags')
|
||||
if not props.has_thumbnail:
|
||||
write_to_report(props, 'Add thumbnail:')
|
||||
|
||||
props.report += props.thumbnail_generating_state + '\n'
|
||||
if props.engine == 'NONE':
|
||||
write_to_report(props, 'Set at least one rendering/output engine')
|
||||
|
||||
|
||||
def check_missing_data_material(props):
|
||||
props.report = ''
|
||||
autothumb.update_upload_material_preview(None, None)
|
||||
if props.name == '':
|
||||
write_to_report(props, 'Set material name')
|
||||
# if props.tags == '':
|
||||
# write_to_report(props, 'Write at least 3 tags')
|
||||
if not props.has_thumbnail:
|
||||
write_to_report(props, 'Add thumbnail:')
|
||||
props.report += props.thumbnail_generating_state
|
||||
|
@ -124,16 +107,56 @@ def check_missing_data_material(props):
|
|||
|
||||
def check_missing_data_brush(props):
|
||||
autothumb.update_upload_brush_preview(None, None)
|
||||
props.report = ''
|
||||
if props.name == '':
|
||||
write_to_report(props, 'Set brush name')
|
||||
# if props.tags == '':
|
||||
# write_to_report(props, 'Write at least 3 tags')
|
||||
if not props.has_thumbnail:
|
||||
write_to_report(props, 'Add thumbnail:')
|
||||
props.report += props.thumbnail_generating_state
|
||||
|
||||
|
||||
def check_missing_data(asset_type, props):
|
||||
'''
|
||||
checks if user did everything allright for particular assets and notifies him back if not.
|
||||
Parameters
|
||||
----------
|
||||
asset_type
|
||||
props
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
'''
|
||||
props.report = ''
|
||||
|
||||
if props.name == '':
|
||||
write_to_report(props, f'Set {asset_type.lower()} name.\n'
|
||||
f'It has to be in English and \n'
|
||||
f'can not be longer than 40 letters.\n')
|
||||
if len(props.name) > 40:
|
||||
write_to_report(props, f'The name is too long. maximum is 40 letters')
|
||||
|
||||
if props.is_private == 'PUBLIC':
|
||||
|
||||
if len(props.description) < 20:
|
||||
write_to_report(props, "The description is too short or empty. \n"
|
||||
"Please write a description that describes \n "
|
||||
"your asset as good as possible.\n"
|
||||
"Description helps to bring your asset up\n in relevant search results. ")
|
||||
if props.tags == '':
|
||||
write_to_report(props, 'Write at least 3 tags.\n'
|
||||
'Tags help to bring your asset up in relevant search results.')
|
||||
|
||||
if asset_type == 'MODEL':
|
||||
check_missing_data_model(props)
|
||||
if asset_type == 'SCENE':
|
||||
check_missing_data_scene(props)
|
||||
elif asset_type == 'MATERIAL':
|
||||
check_missing_data_material(props)
|
||||
elif asset_type == 'BRUSH':
|
||||
check_missing_data_brush(props)
|
||||
|
||||
if props.report != '':
|
||||
props.report = f'Please fix these issues before {props.is_private.lower()} upload:\n\n' + props.report
|
||||
|
||||
|
||||
def sub_to_camel(content):
|
||||
replaced = re.sub(r"_.",
|
||||
lambda m: m.group(0)[1].upper(), content)
|
||||
|
@ -514,8 +537,6 @@ def patch_individual_metadata(asset_id, metadata_dict, api_key):
|
|||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
|
||||
# class OBJECT_MT_blenderkit_fast_metadata_menu(bpy.types.Menu):
|
||||
# bl_label = "Fast category change"
|
||||
# bl_idname = "OBJECT_MT_blenderkit_fast_metadata_menu"
|
||||
|
@ -540,13 +561,14 @@ def update_free_full(self, context):
|
|||
if self.asset_type == 'material':
|
||||
if self.free_full == 'FULL':
|
||||
self.free_full = 'FREE'
|
||||
ui_panels.ui_message(title = "All BlenderKit materials are free",
|
||||
message = "Any material uploaded to BlenderKit is free." \
|
||||
" However, it can still earn money for the author," \
|
||||
" based on our fair share system. " \
|
||||
"Part of subscription is sent to artists based on usage by paying users.")
|
||||
ui_panels.ui_message(title="All BlenderKit materials are free",
|
||||
message="Any material uploaded to BlenderKit is free." \
|
||||
" However, it can still earn money for the author," \
|
||||
" based on our fair share system. " \
|
||||
"Part of subscription is sent to artists based on usage by paying users.")
|
||||
|
||||
def can_edit_asset(active_index = -1, asset_data = None):
|
||||
|
||||
def can_edit_asset(active_index=-1, asset_data=None):
|
||||
if active_index == -1 and not asset_data:
|
||||
return False
|
||||
profile = bpy.context.window_manager.get('bkit profile')
|
||||
|
@ -562,6 +584,7 @@ def can_edit_asset(active_index = -1, asset_data = None):
|
|||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FastMetadata(bpy.types.Operator):
|
||||
"""Fast change of the category of object directly in asset bar."""
|
||||
bl_idname = "wm.blenderkit_fast_metadata"
|
||||
|
@ -597,7 +620,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
name="Subcategory",
|
||||
description="main category to put into",
|
||||
items=categories.get_subcategory_enums,
|
||||
update = categories.update_subcategory_enums
|
||||
update=categories.update_subcategory_enums
|
||||
)
|
||||
subcategory1: EnumProperty(
|
||||
name="Subcategory",
|
||||
|
@ -620,7 +643,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
default="PUBLIC",
|
||||
)
|
||||
|
||||
free_full:EnumProperty(
|
||||
free_full: EnumProperty(
|
||||
name="Free or Full Plan",
|
||||
items=(
|
||||
('FREE', 'Free', "You consent you want to release this asset as free for everyone"),
|
||||
|
@ -648,7 +671,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
layout.prop(self, 'subcategory')
|
||||
if self.subcategory != 'NONE' and self.subcategory1 != 'NONE':
|
||||
enums = categories.get_subcategory1_enums(self, context)
|
||||
if enums[0][0]!='NONE':
|
||||
if enums[0][0] != 'NONE':
|
||||
layout.prop(self, 'subcategory1')
|
||||
layout.prop(self, 'name')
|
||||
layout.prop(self, 'description')
|
||||
|
@ -658,8 +681,6 @@ class FastMetadata(bpy.types.Operator):
|
|||
if self.is_private == 'PUBLIC':
|
||||
layout.prop(self, 'license')
|
||||
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
|
||||
props = bpy.context.scene.blenderkitUI
|
||||
|
@ -685,7 +706,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
args=(self.asset_id, mdict, user_preferences.api_key))
|
||||
thread.start()
|
||||
tasks_queue.add_task((ui.add_report, (f'Uploading metadata for {self.name}. '
|
||||
f'Refreash search results to see that changes applied correctly.', 8,)))
|
||||
f'Refresh search results to see that changes applied correctly.', 8,)))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
@ -730,7 +751,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
|
||||
wm = context.window_manager
|
||||
|
||||
return wm.invoke_props_dialog(self, width = 600)
|
||||
return wm.invoke_props_dialog(self, width=600)
|
||||
|
||||
|
||||
def verification_status_change_thread(asset_id, state, api_key):
|
||||
|
@ -944,7 +965,7 @@ class Uploader(threading.Thread):
|
|||
}
|
||||
datafile = os.path.join(self.export_data['temp_dir'], BLENDERKIT_EXPORT_DATA_FILE)
|
||||
|
||||
with open(datafile, 'w', encoding = 'utf-8') as s:
|
||||
with open(datafile, 'w', encoding='utf-8') as s:
|
||||
json.dump(data, s, ensure_ascii=False, indent=4)
|
||||
|
||||
# non waiting method - not useful here..
|
||||
|
@ -987,7 +1008,7 @@ class Uploader(threading.Thread):
|
|||
})
|
||||
|
||||
if not os.path.exists(fpath):
|
||||
self.send_message ("File packing failed, please try manual packing first")
|
||||
self.send_message("File packing failed, please try manual packing first")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.send_message('Uploading files')
|
||||
|
@ -1018,28 +1039,6 @@ class Uploader(threading.Thread):
|
|||
return {'CANCELLED'}
|
||||
|
||||
|
||||
def check_missing_data(asset_type, props):
|
||||
'''
|
||||
checks if user did everything allright for particular assets and notifies him back if not.
|
||||
Parameters
|
||||
----------
|
||||
asset_type
|
||||
props
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
'''
|
||||
if asset_type == 'MODEL':
|
||||
check_missing_data_model(props)
|
||||
if asset_type == 'SCENE':
|
||||
check_missing_data_scene(props)
|
||||
elif asset_type == 'MATERIAL':
|
||||
check_missing_data_material(props)
|
||||
elif asset_type == 'BRUSH':
|
||||
check_missing_data_brush(props)
|
||||
|
||||
|
||||
def start_upload(self, context, asset_type, reupload, upload_set):
|
||||
'''start upload process, by processing data, then start a thread that cares about the rest of the upload.'''
|
||||
|
||||
|
@ -1065,7 +1064,6 @@ def start_upload(self, context, asset_type, reupload, upload_set):
|
|||
check_missing_data(asset_type, props)
|
||||
# if previous check did find any problems then
|
||||
if props.report != '':
|
||||
self.report({'ERROR_INVALID_INPUT'}, props.report)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not reupload:
|
||||
|
@ -1188,14 +1186,18 @@ class UploadOperator(Operator):
|
|||
if self.main_file:
|
||||
upload_set.append('MAINFILE')
|
||||
|
||||
#this is accessed later in get_upload_data and needs to be written.
|
||||
# this is accessed later in get_upload_data and needs to be written.
|
||||
# should pass upload_set all the way to it probably
|
||||
if 'MAINFILE' in upload_set:
|
||||
self.main_file = True
|
||||
|
||||
result = start_upload(self, context, self.asset_type, self.reupload, upload_set=upload_set, )
|
||||
|
||||
return {'FINISHED'}
|
||||
if props.report != '':
|
||||
# self.report({'ERROR_INVALID_INPUT'}, props.report)
|
||||
self.report({'ERROR_INVALID_CONTEXT'}, props.report)
|
||||
|
||||
return result
|
||||
|
||||
def draw(self, context):
|
||||
props = utils.get_upload_props()
|
||||
|
|
|
@ -91,6 +91,11 @@ def upload_file(upload_data, f):
|
|||
|
||||
if 250 > upload_response.status_code > 199:
|
||||
uploaded = True
|
||||
upload_done_url = paths.get_api_url() + 'uploads_s3/' + upload['id'] + '/upload-file/'
|
||||
upload_response = rerequests.post(upload_done_url, headers=headers, verify=True)
|
||||
print(upload_response)
|
||||
tasks_queue.add_task((ui.add_report, (f"Finished file upload{os.path.basename(f['file_path'])}",)))
|
||||
return True
|
||||
else:
|
||||
print(upload_response.text)
|
||||
message = f"Upload failed, retry. File : {f['type']} {os.path.basename(f['file_path'])}"
|
||||
|
@ -103,13 +108,11 @@ def upload_file(upload_data, f):
|
|||
time.sleep(1)
|
||||
|
||||
# confirm single file upload to bkit server
|
||||
print(upload)
|
||||
upload_done_url = paths.get_api_url() + 'uploads_s3/' + upload['id'] + '/upload-file/'
|
||||
upload_response = rerequests.post(upload_done_url, headers=headers, verify=True)
|
||||
|
||||
tasks_queue.add_task((ui.add_report, (f"Finished file upload{os.path.basename(f['file_path'])}",)))
|
||||
|
||||
return uploaded
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def upload_files(upload_data, files):
|
||||
|
|
|
@ -366,12 +366,12 @@ def get_thumbnail(name):
|
|||
|
||||
|
||||
def files_size_to_text(size):
|
||||
fsmb = size // (1024 * 1024)
|
||||
fsmb = size / (1024 * 1024)
|
||||
fskb = size % 1024
|
||||
if fsmb == 0:
|
||||
return f'{fskb}KB'
|
||||
return f'{round(fskb)}KB'
|
||||
else:
|
||||
return f'{fsmb}MB {fskb}KB'
|
||||
return f'{round(fsmb, 1)}MB'
|
||||
|
||||
|
||||
def get_brush_props(context):
|
||||
|
|
|
@ -560,10 +560,12 @@ def register():
|
|||
|
||||
# Add shortcuts to the keymap.
|
||||
wm = bpy.context.window_manager
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Pose')
|
||||
kmi = km.keymap_items.new('wm.call_menu', 'W', 'PRESS', alt=True, shift=True)
|
||||
kmi.properties.name = 'POSE_MT_selection_sets_select'
|
||||
addon_keymaps.append((km, kmi))
|
||||
if wm.keyconfigs.addon is not None:
|
||||
# wm.keyconfigs.addon is None when Blender is running in the background.
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Pose')
|
||||
kmi = km.keymap_items.new('wm.call_menu', 'W', 'PRESS', alt=True, shift=True)
|
||||
kmi.properties.name = 'POSE_MT_selection_sets_select'
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
# Add entries to menus.
|
||||
bpy.types.VIEW3D_MT_select_pose.append(menu_func_select_selection_set)
|
||||
|
|
|
@ -21,7 +21,7 @@ bl_info = {
|
|||
"name": "Grease Pencil Tools",
|
||||
"description": "Extra tools for Grease Pencil",
|
||||
"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
|
||||
"version": (1, 3, 4),
|
||||
"version": (1, 4, 0),
|
||||
"blender": (2, 91, 0),
|
||||
"location": "Sidebar > Grease Pencil > Grease Pencil Tools",
|
||||
"warning": "",
|
||||
|
|
|
@ -123,6 +123,7 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
self.key = prefs.keycode
|
||||
self.evaluate_gp_obj_key = prefs.evaluate_gp_obj_key
|
||||
self.always_snap = prefs.always_snap
|
||||
self.rolling_mode = prefs.rolling_mode
|
||||
|
||||
self.dpi = context.preferences.system.dpi
|
||||
self.ui_scale = context.preferences.system.ui_scale
|
||||
|
@ -146,7 +147,7 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
self.init_mouse_x = self.cursor_x = event.mouse_region_x
|
||||
|
||||
# self.init_mouse_y = event.mouse_region_y # only to display init frame text
|
||||
self.init_frame = self.new_frame = context.scene.frame_current
|
||||
self.cancel_frame = self.init_frame = self.new_frame = context.scene.frame_current
|
||||
self.lock_range = context.scene.lock_frame_selection_to_range
|
||||
if context.scene.use_preview_range:
|
||||
self.f_start = context.scene.frame_preview_start
|
||||
|
@ -195,11 +196,28 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
if not ob or not self.pos:
|
||||
# Disable inverted behavior if no frame to snap
|
||||
self.always_snap = False
|
||||
if self.rolling_mode:
|
||||
self.report({'WARNING'}, 'No Keys to flip on')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.rolling_mode:
|
||||
# sorted and casted to int list since it's going to work with indexes
|
||||
self.pos = sorted([int(f) for f in self.pos])
|
||||
# find and make current frame the "starting" frame (force snap)
|
||||
active_pos = [i for i, num in enumerate(self.pos) if num <= self.init_frame]
|
||||
if active_pos:
|
||||
self.init_index = active_pos[-1]
|
||||
self.init_frame = self.new_frame = self.pos[self.init_index]
|
||||
else:
|
||||
self.init_index = 0
|
||||
self.init_frame = self.new_frame = self.pos[0]
|
||||
|
||||
# del active_pos
|
||||
self.index_limit = len(self.pos) - 1
|
||||
|
||||
# Also snap on play bounds (sliced off for keyframe display)
|
||||
self.pos += [self.f_start, self.f_end]
|
||||
|
||||
|
||||
# Disable Onion skin
|
||||
self.active_space_data = context.space_data
|
||||
self.onion_skin = None
|
||||
|
@ -241,43 +259,44 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
|
||||
self.hud_lines = []
|
||||
|
||||
# frame marks
|
||||
for x in hud_pos_x:
|
||||
self.hud_lines.append((x, my - (frame_height/2)))
|
||||
self.hud_lines.append((x, my + (frame_height/2)))
|
||||
if not self.rolling_mode:
|
||||
# frame marks
|
||||
for x in hud_pos_x:
|
||||
self.hud_lines.append((x, my - (frame_height/2)))
|
||||
self.hud_lines.append((x, my + (frame_height/2)))
|
||||
|
||||
# init frame mark
|
||||
self.hud_lines += [(self.init_mouse_x, my - (init_height/2)),
|
||||
(self.init_mouse_x, my + (init_height/2))]
|
||||
|
||||
# Add start/end boundary bracket to HUD
|
||||
if not self.rolling_mode:
|
||||
# Add start/end boundary bracket to HUD
|
||||
start_x = self.init_mouse_x + \
|
||||
(self.f_start - self.init_frame) * self.px_step
|
||||
end_x = self.init_mouse_x + \
|
||||
(self.f_end - self.init_frame) * self.px_step
|
||||
|
||||
start_x = self.init_mouse_x + \
|
||||
(self.f_start - self.init_frame) * self.px_step
|
||||
end_x = self.init_mouse_x + \
|
||||
(self.f_end - self.init_frame) * self.px_step
|
||||
# start
|
||||
up = (start_x, my - (bound_h/2))
|
||||
dn = (start_x, my + (bound_h/2))
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append(dn)
|
||||
|
||||
# start
|
||||
up = (start_x, my - (bound_h/2))
|
||||
dn = (start_x, my + (bound_h/2))
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append(dn)
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append((up[0] + bound_bracket_l, up[1]))
|
||||
self.hud_lines.append(dn)
|
||||
self.hud_lines.append((dn[0] + bound_bracket_l, dn[1]))
|
||||
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append((up[0] + bound_bracket_l, up[1]))
|
||||
self.hud_lines.append(dn)
|
||||
self.hud_lines.append((dn[0] + bound_bracket_l, dn[1]))
|
||||
# end
|
||||
up = (end_x, my - (bound_h/2))
|
||||
dn = (end_x, my + (bound_h/2))
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append(dn)
|
||||
|
||||
# end
|
||||
up = (end_x, my - (bound_h/2))
|
||||
dn = (end_x, my + (bound_h/2))
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append(dn)
|
||||
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append((up[0] - bound_bracket_l, up[1]))
|
||||
self.hud_lines.append(dn)
|
||||
self.hud_lines.append((dn[0] - bound_bracket_l, dn[1]))
|
||||
self.hud_lines.append(up)
|
||||
self.hud_lines.append((up[0] - bound_bracket_l, up[1]))
|
||||
self.hud_lines.append(dn)
|
||||
self.hud_lines.append((dn[0] - bound_bracket_l, dn[1]))
|
||||
|
||||
# Horizontal line
|
||||
self.hud_lines += [(0, my), (width, my)]
|
||||
|
@ -286,16 +305,24 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') # initiate shader
|
||||
self.batch_timeline = batch_for_shader(
|
||||
shader, 'LINES', {"pos": self.hud_lines})
|
||||
|
||||
if self.rolling_mode:
|
||||
current_id = self.pos.index(self.new_frame)
|
||||
# Add init_frame to "cancel" it in later UI code
|
||||
ui_key_pos = [i - current_id + self.init_frame for i, _f in enumerate(self.pos[:-2])]
|
||||
else:
|
||||
ui_key_pos = self.pos[:-2]
|
||||
|
||||
|
||||
# keyframe display
|
||||
if self.keyframe_aspect == 'LINE':
|
||||
key_lines = []
|
||||
# Slice off position of start/end frame added last (in list for snapping)
|
||||
for i in self.pos[:-2]:
|
||||
for i in ui_key_pos:
|
||||
key_lines.append(
|
||||
(self.init_mouse_x + ((i-self.init_frame) * self.px_step), my - (key_height/2)))
|
||||
key_lines.append(
|
||||
(self.init_mouse_x + ((i-self.init_frame)*self.px_step), my + (key_height/2)))
|
||||
(self.init_mouse_x + ((i-self.init_frame) * self.px_step), my + (key_height/2)))
|
||||
|
||||
self.batch_keyframes = batch_for_shader(
|
||||
shader, 'LINES', {"pos": key_lines})
|
||||
|
@ -309,7 +336,7 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
shaped_key = []
|
||||
indices = []
|
||||
idx_offset = 0
|
||||
for i in self.pos[:-2]:
|
||||
for i in ui_key_pos:
|
||||
center = self.init_mouse_x + ((i-self.init_frame)*self.px_step)
|
||||
if self.keyframe_aspect == 'DIAMOND':
|
||||
# +1 on x is to correct pixel alignement
|
||||
|
@ -339,6 +366,8 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
# convert frame list to array for numpy snap utility
|
||||
self.pos = np.asarray(self.pos)
|
||||
|
||||
if self.rolling_mode:
|
||||
context.scene.frame_current = self.new_frame
|
||||
|
||||
args = (self, context)
|
||||
self.viewtype = None
|
||||
|
@ -383,42 +412,52 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
self.offset = int(px_offset / self.px_step)
|
||||
self.new_frame = self.init_frame + self.offset
|
||||
|
||||
mod_snap = False
|
||||
if self.snap_ctrl and event.ctrl:
|
||||
mod_snap = True
|
||||
if self.snap_shift and event.shift:
|
||||
mod_snap = True
|
||||
if self.snap_alt and event.alt:
|
||||
mod_snap = True
|
||||
if self.rolling_mode:
|
||||
# Frame Flipping mode (equidistant scrub snap)
|
||||
self.index = self.init_index + self.offset
|
||||
# clamp to possible index range
|
||||
self.index = min(max(self.index, 0), self.index_limit)
|
||||
self.new_frame = self.pos[self.index]
|
||||
context.scene.frame_current = self.new_frame
|
||||
self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
|
||||
|
||||
## Snapping
|
||||
if self.always_snap:
|
||||
# inverted snapping behavior
|
||||
if not self.snap_on and not mod_snap:
|
||||
self.new_frame = nearest(self.pos, self.new_frame)
|
||||
else:
|
||||
if self.snap_on or mod_snap:
|
||||
self.new_frame = nearest(self.pos, self.new_frame)
|
||||
mod_snap = False
|
||||
if self.snap_ctrl and event.ctrl:
|
||||
mod_snap = True
|
||||
if self.snap_shift and event.shift:
|
||||
mod_snap = True
|
||||
if self.snap_alt and event.alt:
|
||||
mod_snap = True
|
||||
|
||||
# frame range restriction
|
||||
if self.lock_range:
|
||||
if self.new_frame < self.f_start:
|
||||
self.new_frame = self.f_start
|
||||
elif self.new_frame > self.f_end:
|
||||
self.new_frame = self.f_end
|
||||
## Snapping
|
||||
if self.always_snap:
|
||||
# inverted snapping behavior
|
||||
if not self.snap_on and not mod_snap:
|
||||
self.new_frame = nearest(self.pos, self.new_frame)
|
||||
else:
|
||||
if self.snap_on or mod_snap:
|
||||
self.new_frame = nearest(self.pos, self.new_frame)
|
||||
|
||||
# context.scene.frame_set(self.new_frame)
|
||||
context.scene.frame_current = self.new_frame
|
||||
# frame range restriction
|
||||
if self.lock_range:
|
||||
if self.new_frame < self.f_start:
|
||||
self.new_frame = self.f_start
|
||||
elif self.new_frame > self.f_end:
|
||||
self.new_frame = self.f_end
|
||||
|
||||
# - recalculate offset to snap cursor to frame
|
||||
self.offset = self.new_frame - self.init_frame
|
||||
# context.scene.frame_set(self.new_frame)
|
||||
context.scene.frame_current = self.new_frame
|
||||
|
||||
# - calculate cursor pixel position from frame offset
|
||||
self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
|
||||
# - recalculate offset to snap cursor to frame
|
||||
self.offset = self.new_frame - self.init_frame
|
||||
|
||||
# - calculate cursor pixel position from frame offset
|
||||
self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
|
||||
|
||||
if event.type == 'ESC':
|
||||
# frame_set(self.init_frame) ?
|
||||
context.scene.frame_current = self.init_frame
|
||||
context.scene.frame_current = self.cancel_frame
|
||||
self._exit_modal(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
@ -505,6 +544,11 @@ class GPTS_timeline_settings(bpy.types.PropertyGroup):
|
|||
description="Always snap to keys if any, modifier is used deactivate the snapping\nDisabled if no keyframe found",
|
||||
default=False)
|
||||
|
||||
rolling_mode: BoolProperty(
|
||||
name="Rolling Mode",
|
||||
description="Alternative Gap-less timeline. No time informations to quickly roll/flip over keys\nOverride normal and 'always snap' mode",
|
||||
default=False)
|
||||
|
||||
use_in_timeline_editor: BoolProperty(
|
||||
name="Shortcut in timeline editors",
|
||||
description="Add the same shortcut to scrub in timeline editor windows",
|
||||
|
@ -666,7 +710,7 @@ def draw_ts_pref(prefs, layout):
|
|||
snap_text = 'Disable keyframes snap: '
|
||||
else:
|
||||
snap_text = 'Keyframes snap: '
|
||||
|
||||
|
||||
snap_text += 'Left Mouse' if prefs.keycode == 'RIGHTMOUSE' else 'Right Mouse'
|
||||
if not prefs.use_ctrl:
|
||||
snap_text += ' or Ctrl'
|
||||
|
@ -674,12 +718,18 @@ def draw_ts_pref(prefs, layout):
|
|||
snap_text += ' or Shift'
|
||||
if not prefs.use_alt:
|
||||
snap_text += ' or Alt'
|
||||
|
||||
if prefs.rolling_mode:
|
||||
snap_text = 'Gap-less mode (always snap)'
|
||||
|
||||
box.label(text=snap_text, icon='SNAP_ON')
|
||||
if prefs.keycode in ('LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE') and not prefs.use_ctrl and not prefs.use_alt and not prefs.use_shift:
|
||||
box.label(
|
||||
text="Recommended to choose at least one modifier to combine with clicks (default: Ctrl+Alt)", icon="ERROR")
|
||||
|
||||
box.prop(prefs, 'always_snap')
|
||||
row = box.row()
|
||||
row.prop(prefs, 'always_snap')
|
||||
row.prop(prefs, 'rolling_mode')
|
||||
box.prop(prefs, 'use_in_timeline_editor',
|
||||
text='Add same shortcut to scrub within timeline editors')
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ bl_info = {
|
|||
"name": "Collection Manager",
|
||||
"description": "Manage collections and their objects",
|
||||
"author": "Ryan Inch",
|
||||
"version": (2, 19, 3),
|
||||
"version": (2, 21, 0),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "View3D - Object Mode (Shortcut - M)",
|
||||
"warning": '', # used for warning icon and text in addons panel
|
||||
|
|
|
@ -77,6 +77,7 @@ addon_disable_objects_hotkey_keymaps = []
|
|||
classes = (
|
||||
internals.CMListCollection,
|
||||
internals.CMSendReport,
|
||||
internals.CMUISeparatorButton,
|
||||
operators.SetActiveCollection,
|
||||
operators.ExpandAllOperator,
|
||||
operators.ExpandSublevelOperator,
|
||||
|
@ -104,6 +105,8 @@ classes = (
|
|||
operators.CMDisableObjectsOperator,
|
||||
operators.CMDisableUnSelectedObjectsOperator,
|
||||
operators.CMRestoreDisabledObjectsOperator,
|
||||
operators.CMUndoWrapper,
|
||||
operators.CMRedoWrapper,
|
||||
preferences.CMPreferences,
|
||||
ui.CM_UL_items,
|
||||
ui.CollectionManager,
|
||||
|
|
|
@ -621,6 +621,80 @@ def generate_state(*, qcd=False):
|
|||
return state
|
||||
|
||||
|
||||
def check_state(context, *, cm_popup=False, phantom_mode=False, qcd=False):
|
||||
view_layer = context.view_layer
|
||||
|
||||
# check if expanded & history/buffer state still correct
|
||||
if cm_popup and collection_state:
|
||||
new_state = generate_state()
|
||||
|
||||
if new_state["name"] != collection_state["name"]:
|
||||
copy_buffer["RTO"] = ""
|
||||
copy_buffer["values"].clear()
|
||||
|
||||
swap_buffer["A"]["RTO"] = ""
|
||||
swap_buffer["A"]["values"].clear()
|
||||
swap_buffer["B"]["RTO"] = ""
|
||||
swap_buffer["B"]["values"].clear()
|
||||
|
||||
for name in list(expanded):
|
||||
laycol = layer_collections.get(name)
|
||||
if not laycol or not laycol["has_children"]:
|
||||
expanded.remove(name)
|
||||
|
||||
for name in list(expand_history["history"]):
|
||||
laycol = layer_collections.get(name)
|
||||
if not laycol or not laycol["has_children"]:
|
||||
expand_history["history"].remove(name)
|
||||
|
||||
for rto, history in rto_history.items():
|
||||
if view_layer.name in history:
|
||||
del history[view_layer.name]
|
||||
|
||||
|
||||
else:
|
||||
for rto in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
|
||||
if new_state[rto] != collection_state[rto]:
|
||||
if view_layer.name in rto_history[rto]:
|
||||
del rto_history[rto][view_layer.name]
|
||||
|
||||
if view_layer.name in rto_history[rto+"_all"]:
|
||||
del rto_history[rto+"_all"][view_layer.name]
|
||||
|
||||
|
||||
if phantom_mode:
|
||||
cm = context.scene.collection_manager
|
||||
|
||||
# check if in phantom mode and if it's still viable
|
||||
if cm.in_phantom_mode:
|
||||
if layer_collections.keys() != phantom_history["initial_state"].keys():
|
||||
cm.in_phantom_mode = False
|
||||
|
||||
if view_layer.name != phantom_history["view_layer"]:
|
||||
cm.in_phantom_mode = False
|
||||
|
||||
if not cm.in_phantom_mode:
|
||||
for key, value in phantom_history.items():
|
||||
try:
|
||||
value.clear()
|
||||
except AttributeError:
|
||||
if key == "view_layer":
|
||||
phantom_history["view_layer"] = ""
|
||||
|
||||
|
||||
if qcd and qcd_collection_state:
|
||||
from .qcd_operators import QCDAllBase
|
||||
new_state = generate_state(qcd=True)
|
||||
|
||||
if (new_state["name"] != qcd_collection_state["name"]
|
||||
or new_state["exclude"] != qcd_collection_state["exclude"]
|
||||
or new_state["qcd"] != qcd_collection_state["qcd"]):
|
||||
if view_layer.name in qcd_history:
|
||||
del qcd_history[view_layer.name]
|
||||
qcd_collection_state.clear()
|
||||
QCDAllBase.clear()
|
||||
|
||||
|
||||
def get_move_selection(*, names_only=False):
|
||||
global move_selection
|
||||
|
||||
|
@ -720,3 +794,30 @@ def send_report(message):
|
|||
bpy.ops.view3d.cm_send_report(ctx, 'INVOKE_DEFAULT', message=message)
|
||||
|
||||
bpy.app.timers.register(report)
|
||||
|
||||
|
||||
class CMUISeparatorButton(Operator):
|
||||
bl_label = "UI Separator Button"
|
||||
bl_idname = "view3d.cm_ui_separator_button"
|
||||
|
||||
def execute(self, context):
|
||||
return {'CANCELED'}
|
||||
|
||||
def add_vertical_separator_line(row):
|
||||
# add buffer before to account for scaling
|
||||
separator = row.row()
|
||||
separator.scale_x = 0.1
|
||||
separator.label()
|
||||
|
||||
# add separator line
|
||||
separator = row.row()
|
||||
separator.scale_x = 0.2
|
||||
separator.enabled = False
|
||||
separator.operator("view3d.cm_ui_separator_button",
|
||||
text="",
|
||||
icon='BLANK1',
|
||||
)
|
||||
# add buffer after to account for scaling
|
||||
separator = row.row()
|
||||
separator.scale_x = 0.1
|
||||
separator.label()
|
||||
|
|
|
@ -38,6 +38,8 @@ from . import internals
|
|||
# For FUNCTIONS
|
||||
from .internals import (
|
||||
update_property_group,
|
||||
generate_state,
|
||||
check_state,
|
||||
get_modifiers,
|
||||
get_move_selection,
|
||||
get_move_active,
|
||||
|
@ -1499,3 +1501,48 @@ class CMRestoreDisabledObjectsOperator(Operator):
|
|||
obj.select_set(True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CMUndoWrapper(Operator):
|
||||
bl_label = "Undo"
|
||||
bl_description = "Undo previous action"
|
||||
bl_idname = "view3d.undo_wrapper"
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
return bpy.ops.ed.undo.poll()
|
||||
|
||||
def execute(self, context):
|
||||
internals.collection_state.clear()
|
||||
internals.collection_state.update(generate_state())
|
||||
bpy.ops.ed.undo()
|
||||
update_property_group(context)
|
||||
|
||||
check_state(context, cm_popup=True)
|
||||
|
||||
# clear buffers
|
||||
internals.copy_buffer["RTO"] = ""
|
||||
internals.copy_buffer["values"].clear()
|
||||
|
||||
internals.swap_buffer["A"]["RTO"] = ""
|
||||
internals.swap_buffer["A"]["values"].clear()
|
||||
internals.swap_buffer["B"]["RTO"] = ""
|
||||
internals.swap_buffer["B"]["values"].clear()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CMRedoWrapper(Operator):
|
||||
bl_label = "Redo"
|
||||
bl_description = "Redo previous action"
|
||||
bl_idname = "view3d.redo_wrapper"
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
return bpy.ops.ed.redo.poll()
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.ed.redo()
|
||||
update_property_group(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
|
|
@ -40,16 +40,13 @@ from .internals import (
|
|||
update_collection_tree,
|
||||
update_property_group,
|
||||
generate_state,
|
||||
check_state,
|
||||
get_move_selection,
|
||||
get_move_active,
|
||||
update_qcd_header,
|
||||
add_vertical_separator_line,
|
||||
)
|
||||
|
||||
from .qcd_operators import (
|
||||
QCDAllBase,
|
||||
)
|
||||
|
||||
|
||||
preview_collections = {}
|
||||
last_icon_theme_text = None
|
||||
last_icon_theme_text_sel = None
|
||||
|
@ -79,6 +76,7 @@ class CollectionManager(Operator):
|
|||
cm = context.scene.collection_manager
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
view_layer = context.view_layer
|
||||
collection = context.view_layer.layer_collection.collection
|
||||
|
||||
if view_layer.name != cls.last_view_layer:
|
||||
if prefs.enable_qcd:
|
||||
|
@ -136,6 +134,11 @@ class CollectionManager(Operator):
|
|||
renum_sec.alignment = 'LEFT'
|
||||
renum_sec.operator("view3d.renumerate_qcd_slots")
|
||||
|
||||
undo_sec = op_sec.row(align=True)
|
||||
undo_sec.alignment = 'LEFT'
|
||||
undo_sec.operator("view3d.undo_wrapper", text="", icon='LOOP_BACK')
|
||||
undo_sec.operator("view3d.redo_wrapper", text="", icon='LOOP_FORWARDS')
|
||||
|
||||
# menu & filter
|
||||
right_sec = button_row_1.row()
|
||||
right_sec.alignment = 'RIGHT'
|
||||
|
@ -162,16 +165,52 @@ class CollectionManager(Operator):
|
|||
master_collection_row.separator()
|
||||
|
||||
# name
|
||||
name_row = master_collection_row.row()
|
||||
name_row.prop(self, "master_collection", text='')
|
||||
name_row.enabled = False
|
||||
name_row = master_collection_row.row(align=True)
|
||||
name_field = name_row.row(align=True)
|
||||
name_field.prop(self, "master_collection", text='')
|
||||
name_field.enabled = False
|
||||
|
||||
# set selection
|
||||
setsel = name_row.row(align=True)
|
||||
icon = 'DOT'
|
||||
|
||||
if any((laycol["ptr"].exclude,
|
||||
collection.hide_select,
|
||||
collection.hide_viewport,
|
||||
laycol["ptr"].hide_viewport,
|
||||
not collection.objects,)):
|
||||
# objects cannot be selected
|
||||
setsel.active = False
|
||||
|
||||
else:
|
||||
for obj in collection.objects:
|
||||
if obj.select_get() == False:
|
||||
# some objects remain unselected
|
||||
icon = 'LAYER_USED'
|
||||
break
|
||||
|
||||
if icon != 'LAYER_USED':
|
||||
# all objects are selected
|
||||
icon = 'LAYER_ACTIVE'
|
||||
|
||||
prop = setsel.operator("view3d.select_collection_objects",
|
||||
text="",
|
||||
icon=icon,
|
||||
depress=bool(icon == 'LAYER_ACTIVE')
|
||||
)
|
||||
prop.is_master_collection = True
|
||||
prop.collection_name = 'Master Collection'
|
||||
|
||||
master_collection_row.separator()
|
||||
|
||||
# global rtos
|
||||
global_rto_row = master_collection_row.row()
|
||||
global_rto_row.alignment = 'RIGHT'
|
||||
|
||||
# used as a separator (actual separator not wide enough)
|
||||
global_rto_row.label()
|
||||
|
||||
|
||||
# set collection
|
||||
row_setcol = global_rto_row.row()
|
||||
row_setcol.alignment = 'LEFT'
|
||||
row_setcol.operator_context = 'INVOKE_DEFAULT'
|
||||
|
@ -183,7 +222,10 @@ class CollectionManager(Operator):
|
|||
|
||||
collection = context.view_layer.layer_collection.collection
|
||||
|
||||
icon = 'MESH_CUBE'
|
||||
icon = 'GRID'
|
||||
|
||||
if collection.objects:
|
||||
icon = 'MESH_CUBE'
|
||||
|
||||
if selected_objects:
|
||||
if active_object and active_object.name in collection.objects:
|
||||
|
@ -193,13 +235,33 @@ class CollectionManager(Operator):
|
|||
icon = 'STICKY_UVS_LOC'
|
||||
|
||||
else:
|
||||
row_setcol.enabled = False
|
||||
row_setcol.active = False
|
||||
|
||||
|
||||
# add vertical separator line
|
||||
separator = row_setcol.row()
|
||||
separator.scale_x = 0.2
|
||||
separator.enabled = False
|
||||
separator.operator("view3d.cm_ui_separator_button",
|
||||
text="",
|
||||
icon='BLANK1',
|
||||
)
|
||||
|
||||
# add operator
|
||||
prop = row_setcol.operator("view3d.set_collection", text="",
|
||||
icon=icon, emboss=False)
|
||||
prop.is_master_collection = True
|
||||
prop.collection_name = 'Master Collection'
|
||||
|
||||
# add vertical separator line
|
||||
separator = row_setcol.row()
|
||||
separator.scale_x = 0.2
|
||||
separator.enabled = False
|
||||
separator.operator("view3d.cm_ui_separator_button",
|
||||
text="",
|
||||
icon='BLANK1',
|
||||
)
|
||||
|
||||
copy_icon = 'COPYDOWN'
|
||||
swap_icon = 'ARROW_LEFTRIGHT'
|
||||
copy_swap_icon = 'SELECT_INTERSECT'
|
||||
|
@ -387,58 +449,8 @@ class CollectionManager(Operator):
|
|||
if cm.cm_list_index >= len(cm.cm_list_collection):
|
||||
cm.cm_list_index = -1
|
||||
|
||||
# check if expanded & history/buffer state still correct
|
||||
if internals.collection_state:
|
||||
new_state = generate_state()
|
||||
|
||||
if new_state["name"] != internals.collection_state["name"]:
|
||||
internals.copy_buffer["RTO"] = ""
|
||||
internals.copy_buffer["values"].clear()
|
||||
|
||||
internals.swap_buffer["A"]["RTO"] = ""
|
||||
internals.swap_buffer["A"]["values"].clear()
|
||||
internals.swap_buffer["B"]["RTO"] = ""
|
||||
internals.swap_buffer["B"]["values"].clear()
|
||||
|
||||
for name in list(internals.expanded):
|
||||
laycol = internals.layer_collections.get(name)
|
||||
if not laycol or not laycol["has_children"]:
|
||||
internals.expanded.remove(name)
|
||||
|
||||
for name in list(internals.expand_history["history"]):
|
||||
laycol = internals.layer_collections.get(name)
|
||||
if not laycol or not laycol["has_children"]:
|
||||
internals.expand_history["history"].remove(name)
|
||||
|
||||
for rto, history in internals.rto_history.items():
|
||||
if view_layer.name in history:
|
||||
del history[view_layer.name]
|
||||
|
||||
|
||||
else:
|
||||
for rto in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
|
||||
if new_state[rto] != internals.collection_state[rto]:
|
||||
if view_layer.name in internals.rto_history[rto]:
|
||||
del internals.rto_history[rto][view_layer.name]
|
||||
|
||||
if view_layer.name in internals.rto_history[rto+"_all"]:
|
||||
del internals.rto_history[rto+"_all"][view_layer.name]
|
||||
|
||||
# check if in phantom mode and if it's still viable
|
||||
if cm.in_phantom_mode:
|
||||
if internals.layer_collections.keys() != internals.phantom_history["initial_state"].keys():
|
||||
cm.in_phantom_mode = False
|
||||
|
||||
if view_layer.name != internals.phantom_history["view_layer"]:
|
||||
cm.in_phantom_mode = False
|
||||
|
||||
if not cm.in_phantom_mode:
|
||||
for key, value in internals.phantom_history.items():
|
||||
try:
|
||||
value.clear()
|
||||
except AttributeError:
|
||||
if key == "view_layer":
|
||||
internals.phantom_history["view_layer"] = ""
|
||||
# check if history/buffer/phantom state still correct
|
||||
check_state(context, cm_popup=True, phantom_mode=True)
|
||||
|
||||
# handle window sizing
|
||||
max_width = 960
|
||||
|
@ -583,7 +595,8 @@ class CM_UL_items(UIList):
|
|||
QCD.scale_x = 0.4
|
||||
QCD.prop(item, "qcd_slot_idx", text="")
|
||||
|
||||
c_name = row.row()
|
||||
# collection name
|
||||
c_name = row.row(align=True)
|
||||
|
||||
#if rename[0] and index == cm.cm_list_index:
|
||||
#c_name.activate_init = True
|
||||
|
@ -591,16 +604,54 @@ class CM_UL_items(UIList):
|
|||
|
||||
c_name.prop(item, "name", text="", expand=True)
|
||||
|
||||
# set selection
|
||||
setsel = c_name.row(align=True)
|
||||
icon = 'DOT'
|
||||
|
||||
if any((laycol["ptr"].exclude,
|
||||
collection.hide_select,
|
||||
collection.hide_viewport,
|
||||
laycol["ptr"].hide_viewport,
|
||||
not collection.objects,)):
|
||||
# objects cannot be selected
|
||||
setsel.active = False
|
||||
|
||||
else:
|
||||
for obj in collection.objects:
|
||||
if obj.select_get() == False:
|
||||
# some objects remain unselected
|
||||
icon = 'LAYER_USED'
|
||||
break
|
||||
|
||||
if icon != 'LAYER_USED':
|
||||
# all objects are selected
|
||||
icon = 'LAYER_ACTIVE'
|
||||
|
||||
prop = setsel.operator("view3d.select_collection_objects",
|
||||
text="",
|
||||
icon=icon,
|
||||
depress=bool(icon == 'LAYER_ACTIVE')
|
||||
)
|
||||
prop.is_master_collection = False
|
||||
prop.collection_name = item.name
|
||||
|
||||
# used as a separator (actual separator not wide enough)
|
||||
row.label()
|
||||
|
||||
row = s2 if cm.align_local_ops else s1
|
||||
|
||||
|
||||
add_vertical_separator_line(row)
|
||||
|
||||
|
||||
# add set_collection op
|
||||
set_obj_col = row.row()
|
||||
set_obj_col.operator_context = 'INVOKE_DEFAULT'
|
||||
|
||||
icon = 'MESH_CUBE'
|
||||
icon = 'GRID'
|
||||
|
||||
if collection.objects:
|
||||
icon = 'MESH_CUBE'
|
||||
|
||||
if selected_objects:
|
||||
if active_object and active_object.name in collection.objects:
|
||||
|
@ -618,6 +669,8 @@ class CM_UL_items(UIList):
|
|||
prop.is_master_collection = False
|
||||
prop.collection_name = item.name
|
||||
|
||||
add_vertical_separator_line(row)
|
||||
|
||||
|
||||
if cm.show_exclude:
|
||||
exclude_history_base = internals.rto_history["exclude"].get(view_layer.name, {})
|
||||
|
@ -930,17 +983,7 @@ def view3d_header_qcd_slots(self, context):
|
|||
layout = self.layout
|
||||
idx = 1
|
||||
|
||||
if internals.qcd_collection_state:
|
||||
view_layer = context.view_layer
|
||||
new_state = generate_state(qcd=True)
|
||||
|
||||
if (new_state["name"] != internals.qcd_collection_state["name"]
|
||||
or new_state["exclude"] != internals.qcd_collection_state["exclude"]
|
||||
or new_state["qcd"] != internals.qcd_collection_state["qcd"]):
|
||||
if view_layer.name in internals.qcd_history:
|
||||
del internals.qcd_history[view_layer.name]
|
||||
internals.qcd_collection_state.clear()
|
||||
QCDAllBase.clear()
|
||||
check_state(context, qcd=True)
|
||||
|
||||
|
||||
main_row = layout.row(align=True)
|
||||
|
|
|
@ -818,6 +818,7 @@
|
|||
metadatabg="#333333"
|
||||
metadatatext="#b8b8b8"
|
||||
preview_range="#a14d0066"
|
||||
row_alternate="#ffffff0d"
|
||||
>
|
||||
<space>
|
||||
<ThemeSpaceGeneric
|
||||
|
@ -854,6 +855,7 @@
|
|||
<properties>
|
||||
<ThemeProperties
|
||||
match="#c85130"
|
||||
active_modifier="#c85130ff"
|
||||
>
|
||||
<space>
|
||||
<ThemeSpaceGeneric
|
||||
|
@ -962,6 +964,8 @@
|
|||
script_node="#141414"
|
||||
pattern_node="#008db6"
|
||||
layout_node="#3c3c3c"
|
||||
geometry_node="#00d7a4"
|
||||
attribute_node="#3f5980"
|
||||
>
|
||||
<space>
|
||||
<ThemeSpaceGeneric
|
||||
|
@ -1312,6 +1316,42 @@
|
|||
</space>
|
||||
</ThemeStatusBar>
|
||||
</statusbar>
|
||||
<spreadsheet>
|
||||
<ThemeSpreadsheet
|
||||
row_alternate="#4f4f4fff"
|
||||
>
|
||||
<space>
|
||||
<ThemeSpaceGeneric
|
||||
back="#474747"
|
||||
title="#ffffff"
|
||||
text="#c3c3c3"
|
||||
text_hi="#ffffff"
|
||||
header="#454545ff"
|
||||
header_text="#eeeeee"
|
||||
header_text_hi="#ffffff"
|
||||
button="#424242ff"
|
||||
button_title="#ffffff"
|
||||
button_text="#e5e5e5"
|
||||
button_text_hi="#ffffff"
|
||||
navigation_bar="#00000000"
|
||||
execution_buts="#00000000"
|
||||
tab_active="#4b4b4b"
|
||||
tab_inactive="#2b2b2b"
|
||||
tab_back="#232323ff"
|
||||
tab_outline="#232323"
|
||||
>
|
||||
<panelcolors>
|
||||
<ThemePanelColors
|
||||
header="#424242cc"
|
||||
back="#333333b3"
|
||||
sub_back="#0000003e"
|
||||
>
|
||||
</ThemePanelColors>
|
||||
</panelcolors>
|
||||
</ThemeSpaceGeneric>
|
||||
</space>
|
||||
</ThemeSpreadsheet>
|
||||
</spreadsheet>
|
||||
<bone_color_sets>
|
||||
<ThemeBoneColorSet
|
||||
normal="#9a0000"
|
||||
|
|
|
@ -41,6 +41,22 @@ class PIE_MT_SaveOpen(Menu):
|
|||
bl_idname = "PIE_MT_saveopen"
|
||||
bl_label = "Pie Save/Open"
|
||||
|
||||
@staticmethod
|
||||
def _save_as_mainfile_calc_incremental_name():
|
||||
import re
|
||||
dirname, base_name = os.path.split(bpy.data.filepath)
|
||||
base_name_no_ext, ext = os.path.splitext(base_name)
|
||||
match = re.match(r"(.*)_([\d]+)$", base_name_no_ext)
|
||||
if match:
|
||||
prefix, number = match.groups()
|
||||
number = int(number) + 1
|
||||
else:
|
||||
prefix, number = base_name_no_ext, 1
|
||||
prefix = os.path.join(dirname, prefix)
|
||||
while os.path.isfile(output := "%s_%03d%s" % (prefix, number, ext)):
|
||||
number += 1
|
||||
return output
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
@ -57,7 +73,16 @@ class PIE_MT_SaveOpen(Menu):
|
|||
# 9 - TOP - RIGHT
|
||||
pie.operator("wm.save_as_mainfile", text="Save As...", icon='NONE')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator("file.save_incremental", text="Incremental Save", icon='NONE')
|
||||
if bpy.data.is_saved:
|
||||
default_operator_contest = layout.operator_context
|
||||
layout.operator_context = 'EXEC_DEFAULT'
|
||||
pie.operator(
|
||||
"wm.save_as_mainfile", text="Incremental Save", icon='NONE',
|
||||
).filepath = self._save_as_mainfile_calc_incremental_name()
|
||||
layout.operator_context = default_operator_contest
|
||||
else:
|
||||
pie.box().label(text="Incremental Save (unsaved)")
|
||||
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_recover", text="Recovery Menu", icon='RECOVER_LAST')
|
||||
|
||||
|
@ -109,60 +134,8 @@ class PIE_MT_fileio(Menu):
|
|||
box.menu("TOPBAR_MT_file_export", icon='EXPORT')
|
||||
|
||||
|
||||
# Save Incremental
|
||||
class PIE_OT_FileIncrementalSave(Operator):
|
||||
bl_idname = "file.save_incremental"
|
||||
bl_label = "Save Incremental"
|
||||
bl_description = "Save First! then Incremental, .blend will get _001 extension"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (bpy.data.filepath != "")
|
||||
|
||||
def execute(self, context):
|
||||
f_path = bpy.data.filepath
|
||||
b_name = bpy.path.basename(f_path)
|
||||
|
||||
if b_name and b_name.find("_") != -1:
|
||||
# except in cases when there is an underscore in the name like my_file.blend
|
||||
try:
|
||||
str_nb = b_name.rpartition("_")[-1].rpartition(".blend")[0]
|
||||
int_nb = int(str(str_nb))
|
||||
new_nb = str_nb.replace(str(int_nb), str(int_nb + 1))
|
||||
output = f_path.replace(str_nb, new_nb)
|
||||
|
||||
i = 1
|
||||
while os.path.isfile(output):
|
||||
str_nb = b_name.rpartition("_")[-1].rpartition(".blend")[0]
|
||||
i += 1
|
||||
new_nb = str_nb.replace(str(int_nb), str(int_nb + i))
|
||||
output = f_path.replace(str_nb, new_nb)
|
||||
except ValueError:
|
||||
output = f_path.rpartition(".blend")[0] + "_001" + ".blend"
|
||||
else:
|
||||
# no underscore in the name or saving a nameless (.blend) file
|
||||
output = f_path.rpartition(".blend")[0] + "_001" + ".blend"
|
||||
|
||||
# fix for saving in a directory without privileges
|
||||
try:
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output)
|
||||
except:
|
||||
self.report({'WARNING'},
|
||||
"File could not be saved. Check the System Console for errors")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report(
|
||||
{'INFO'}, "File: {0} - Created at: {1}".format(
|
||||
output[len(bpy.path.abspath("//")):],
|
||||
output[:len(bpy.path.abspath("//"))]),
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
PIE_MT_SaveOpen,
|
||||
PIE_OT_FileIncrementalSave,
|
||||
PIE_MT_fileio,
|
||||
PIE_MT_recover,
|
||||
PIE_MT_link,
|
||||
|
|
|
@ -274,17 +274,18 @@ def demo_mode_update():
|
|||
if global_config["anim_screen_switch"]:
|
||||
# print(time_delta, 1)
|
||||
if time_delta > global_config["anim_screen_switch"]:
|
||||
|
||||
screen = bpy.context.window.screen
|
||||
index = bpy.data.screens.keys().index(screen.name)
|
||||
screen_new = bpy.data.screens[(index if index > 0 else len(bpy.data.screens)) - 1]
|
||||
bpy.context.window.screen = screen_new
|
||||
window = bpy.context.window
|
||||
scene = window.scene
|
||||
workspace = window.workspace
|
||||
index = bpy.data.workspaces.keys().index(workspace.name)
|
||||
workspace_new = bpy.data.workspaces[(index + 1) % len(bpy.data.workspaces)]
|
||||
window.workspace = workspace_new
|
||||
|
||||
global_state["last_switch"] = time_current
|
||||
|
||||
# if we also switch scenes then reset last frame
|
||||
# otherwise it could mess up cycle calc.
|
||||
if screen.scene != screen_new.scene:
|
||||
# If we also switch scenes then reset last frame
|
||||
# otherwise it could mess up cycle calculation.
|
||||
if scene != window.scene:
|
||||
global_state["last_frame"] = -1
|
||||
|
||||
#if global_config["mode"] == 'PLAY':
|
||||
|
|
Loading…
Reference in New Issue