1612 lines
60 KiB
Python
1612 lines
60 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import bpy
|
|
from bpy.props import ( # TODO only keep the ones actually used when cleaning
|
|
BoolProperty,
|
|
EnumProperty,
|
|
StringProperty,
|
|
)
|
|
from bpy.types import Operator
|
|
|
|
from . import (
|
|
asset_bar_op,
|
|
asset_inspector,
|
|
autothumb,
|
|
categories,
|
|
client_lib,
|
|
client_tasks,
|
|
global_vars,
|
|
image_utils,
|
|
overrides,
|
|
paths,
|
|
reports,
|
|
ui_panels,
|
|
utils,
|
|
search,
|
|
)
|
|
|
|
|
|
NAME_MINIMUM = 3
|
|
NAME_MAXIMUM = 40
|
|
TAGS_MINIMUM = 3
|
|
TAGS_MAXIMUM = 10
|
|
DESCRIPTION_MINIMUM = 20
|
|
|
|
BLENDERKIT_EXPORT_DATA_FILE = "data.json"
|
|
bk_logger = logging.getLogger(__name__)
|
|
licenses = (
|
|
("royalty_free", "Royalty Free", "royalty free commercial license"),
|
|
("cc_zero", "Creative Commons Zero", "Creative Commons Zero"),
|
|
)
|
|
|
|
|
|
def add_version(data):
|
|
data["sourceAppName"] = "blender"
|
|
data["sourceAppVersion"] = utils.get_blender_version()
|
|
data["addonVersion"] = utils.get_addon_version()
|
|
|
|
|
|
def write_to_report(props, text):
|
|
props.report = props.report + " - " + text + "\n\n"
|
|
|
|
|
|
def prevalidate_model(props):
|
|
"""Check model for possible problems:
|
|
- check if all objects does not have asymmetrical scaling. Asymmetrical scaling is a big problem.
|
|
Anything scaled away from (1,1,1) is a smaller problem. We do not check for that.
|
|
- round minor drifts from 1.0
|
|
"""
|
|
TOLERANCE = 1e-5
|
|
ob = utils.get_active_model()
|
|
obs = utils.get_hierarchy(ob)
|
|
for ob in obs:
|
|
if ob.scale[0] == ob.scale[1] == ob.scale[2]:
|
|
continue # all totally good
|
|
|
|
if all(abs(scalar - 1.0) <= TOLERANCE for scalar in ob.scale):
|
|
bk_logger.info(
|
|
f"Snapped minor float drift on '{ob}': "
|
|
+ f"{ob.scale[0], ob.scale[1], ob.scale[2]} → (1.0, 1.0, 1.0)"
|
|
)
|
|
ob.scale = (1.0, 1.0, 1.0)
|
|
|
|
if ob.scale[0] != ob.scale[1] or ob.scale[1] != ob.scale[2]:
|
|
write_to_report(
|
|
props,
|
|
f"Asymmetrical scaling in the object {ob.name} - please apply scale on all models",
|
|
)
|
|
|
|
|
|
def get_model_materials():
|
|
"""get all materials in the asset hierarchy, will be used to validate materials in future"""
|
|
ob = utils.get_active_model()
|
|
obs = utils.get_hierarchy(ob)
|
|
materials = []
|
|
for ob in obs:
|
|
if ob.type in ("MESH", "CURVE"):
|
|
for mat in ob.data.materials:
|
|
if mat not in materials:
|
|
materials.append(mat)
|
|
return materials
|
|
|
|
|
|
def prevalidate_scene(props):
|
|
"""Check scene for possible problems:
|
|
- check if user is author of all assets in scene"""
|
|
problematic_assets = []
|
|
for ob in bpy.context.scene.objects:
|
|
if not ob.get("asset_data"):
|
|
continue
|
|
if utils.user_is_owner(ob["asset_data"]):
|
|
continue
|
|
asset_name = ob["asset_data"].get("name")
|
|
author_name = ob["asset_data"].get("author", {}).get("fullName")
|
|
problematic_assets.append(f" - {asset_name} by {author_name}\n")
|
|
|
|
if len(problematic_assets) == 0:
|
|
return # No problematic assets found
|
|
|
|
oa_string = "".join(problematic_assets)
|
|
write_to_report(
|
|
props,
|
|
f"Other author's assets are present in scene \n"
|
|
f" Remove assets by these authors before uploading the scene:\n"
|
|
f"{oa_string}",
|
|
)
|
|
|
|
|
|
def check_missing_data_model(props):
|
|
autothumb.update_upload_model_preview(None, None)
|
|
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')
|
|
|
|
|
|
def check_missing_data_scene(props):
|
|
autothumb.update_upload_model_preview(None, None)
|
|
if props.engine == "NONE":
|
|
write_to_report(props, "Set at least one rendering/output engine")
|
|
|
|
|
|
def check_missing_data_material(props):
|
|
autothumb.update_upload_material_preview(None, None)
|
|
if props.engine == "NONE":
|
|
write_to_report(props, "Set rendering/output engine")
|
|
|
|
|
|
def check_missing_data_brush(props):
|
|
autothumb.update_upload_brush_preview(None, None)
|
|
|
|
|
|
def check_missing_data(asset_type, props, upload_set):
|
|
"""Check if all required data is present and fills in the upload props with error messages."""
|
|
props.report = ""
|
|
|
|
if props.name == "":
|
|
write_to_report(
|
|
props,
|
|
"A name is required.\n" " Please provide a name for your asset.",
|
|
)
|
|
elif len(props.name) < NAME_MINIMUM:
|
|
write_to_report(
|
|
props,
|
|
f"Name is too short.\n"
|
|
f" Please provide a name with at least {NAME_MINIMUM} characters.",
|
|
)
|
|
elif len(props.name) > NAME_MAXIMUM:
|
|
write_to_report(
|
|
props,
|
|
f"Name is too long.\n"
|
|
f" Please provide a name with at most {NAME_MAXIMUM} characters.",
|
|
)
|
|
|
|
if props.is_private == "PUBLIC":
|
|
category_ok = props.category == "NONE"
|
|
subcategory_ok = props.subcategory != "EMPTY" and props.subcategory == "NONE"
|
|
subcategory1_ok = props.subcategory1 != "EMPTY" and props.subcategory1 == "NONE"
|
|
if category_ok or subcategory_ok or subcategory1_ok:
|
|
write_to_report(
|
|
props,
|
|
"Category, subcategory, or sub-subcategory has not been selected.\n"
|
|
" Please ensure you select appropriate values; 'None' is not a valid selection.\n"
|
|
" Proper categorization significantly improves your asset's discoverability.",
|
|
)
|
|
|
|
if "THUMBNAIL" in upload_set:
|
|
if asset_type in ("MODEL", "SCENE", "MATERIAL", "PRINTABLE"):
|
|
thumb_path = bpy.path.abspath(props.thumbnail)
|
|
if props.thumbnail == "":
|
|
write_to_report(
|
|
props,
|
|
"A thumbnail image has not been provided.\n"
|
|
" Please add a thumbnail in JPG or PNG format, ensuring at least 1024x1024 pixels.",
|
|
)
|
|
elif not os.path.exists(Path(thumb_path)):
|
|
write_to_report(
|
|
props,
|
|
"Thumbnail filepath does not exist on the disk.\n"
|
|
" Please check the filepath and try again.",
|
|
)
|
|
|
|
if asset_type == "BRUSH":
|
|
brush = utils.get_active_brush()
|
|
if brush is not None:
|
|
thumb_path = bpy.path.abspath(brush.icon_filepath)
|
|
if thumb_path == "":
|
|
write_to_report(
|
|
props,
|
|
"Brush Icon Filepath has not been provided.\n"
|
|
" Please check Custom Icon option add a Brush Icon in JPG or PNG format, ensuring at least 1024x1024 pixels.",
|
|
)
|
|
elif not os.path.exists(Path(thumb_path)):
|
|
write_to_report(
|
|
props,
|
|
"Brush Icon Filepath does not exist on the disk.\n"
|
|
" Please check the filepath and try again.",
|
|
)
|
|
if "PHOTO_THUMBNAIL" in upload_set: # for printable assets
|
|
# Add validation for the photo thumbnail for printable assets
|
|
# only if it's in the upload set
|
|
|
|
if props.photo_thumbnail_will_upload_on_website:
|
|
pass
|
|
else:
|
|
foto_thumb_path = bpy.path.abspath(props.photo_thumbnail)
|
|
if props.photo_thumbnail == "":
|
|
write_to_report(
|
|
props,
|
|
"A photo thumbnail image has not been provided.\n"
|
|
" Please add a photo of the 3D printed object in JPG or PNG format, ensuring at least 1024x1024 pixels.",
|
|
)
|
|
elif not os.path.exists(Path(foto_thumb_path)):
|
|
write_to_report(
|
|
props,
|
|
"Photo thumbnail filepath does not exist on the disk.\n"
|
|
" Please check the filepath and try again.",
|
|
)
|
|
|
|
if props.is_private == "PUBLIC":
|
|
check_public_requirements(props)
|
|
|
|
if asset_type in ("MODEL", "PRINTABLE"):
|
|
prevalidate_model(props)
|
|
check_missing_data_model(props)
|
|
elif asset_type == "SCENE":
|
|
prevalidate_scene(props)
|
|
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"Before {props.is_private.lower()} upload, please fix:\n\n" + props.report
|
|
)
|
|
|
|
|
|
def check_public_requirements(props):
|
|
"""Check requirements for public upload. Add error message into props.report if needed."""
|
|
if props.description == "":
|
|
write_to_report(
|
|
props,
|
|
"No asset description has been provided.\n"
|
|
f" Please write a description of at least {DESCRIPTION_MINIMUM} characters.\n"
|
|
" A comprehensive description enhances your asset's visibility\n"
|
|
" in relevant search results.",
|
|
)
|
|
elif len(props.description) < DESCRIPTION_MINIMUM:
|
|
write_to_report(
|
|
props,
|
|
"The asset description provided is too brief.\n"
|
|
f" Please ensure your description is at least {DESCRIPTION_MINIMUM} characters long.\n"
|
|
" A comprehensive description enhances your asset's visibility\n"
|
|
" in relevant search results.",
|
|
)
|
|
|
|
if props.tags == "":
|
|
write_to_report(
|
|
props,
|
|
"No tags have been provided for your asset.\n"
|
|
f" Please add at least {TAGS_MINIMUM} tags to improve its discoverability.\n"
|
|
" Tags enhance your asset's visibility in relevant search results.",
|
|
)
|
|
elif len(props.tags.split(",")) < TAGS_MINIMUM:
|
|
write_to_report(
|
|
props,
|
|
"Not enough tags have been provided for your asset.\n"
|
|
f" Please ensure you have at least {TAGS_MINIMUM} tags to improve its discoverability.\n"
|
|
" Tags enhance your asset's visibility in relevant search results.",
|
|
)
|
|
|
|
|
|
def check_tags_format(tags_string: str):
|
|
"""Check if tags string is a comma-separated list of tags consisting of only alphanumeric characters and underscores.
|
|
Returns a bool and list of tags that do not meet the format requirement.
|
|
"""
|
|
tags_string = tags_string.strip()
|
|
if tags_string == "":
|
|
return True, []
|
|
tags = tags_string.split(",")
|
|
problematic_tags = []
|
|
for tag in tags:
|
|
tag = tag.strip()
|
|
if tag == "" or not re.match("^[0-9a-zA-Z_]+$", tag):
|
|
problematic_tags.append(tag)
|
|
|
|
if len(problematic_tags) > 0:
|
|
return False, problematic_tags
|
|
|
|
return True, problematic_tags
|
|
|
|
|
|
def sub_to_camel(content):
|
|
replaced = re.sub(r"_.", lambda m: m.group(0)[1].upper(), content)
|
|
return replaced
|
|
|
|
|
|
def get_upload_data(caller=None, context=None, asset_type=None):
|
|
"""
|
|
works though metadata from addom props and prepares it for upload to dicts.
|
|
Parameters
|
|
----------
|
|
caller - upload operator or none
|
|
context - context
|
|
asset_type - asset type in capitals (blender enum)
|
|
|
|
Returns
|
|
-------
|
|
export_ddta- all extra data that the process needs to upload and communicate with UI from a thread.
|
|
- eval_path_computing - string path to UI prop that denots if upload is still running
|
|
- eval_path_state - string path to UI prop that delivers messages about upload to ui
|
|
- eval_path - path to object holding upload data to be able to access it with various further commands
|
|
- models - in case of model upload, list of objects
|
|
- thumbnail_path - path to thumbnail file
|
|
|
|
upload_data - asset_data generated from the ui properties
|
|
|
|
"""
|
|
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
|
export_data = {
|
|
# "type": asset_type,
|
|
}
|
|
upload_params = {}
|
|
if asset_type in ("MODEL", "PRINTABLE"):
|
|
# Prepare to save the file
|
|
mainmodel = utils.get_active_model()
|
|
|
|
props = mainmodel.blenderkit
|
|
|
|
obs = utils.get_hierarchy(mainmodel)
|
|
obnames = []
|
|
for ob in obs:
|
|
obnames.append(ob.name)
|
|
export_data["models"] = obnames
|
|
export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
|
|
|
|
# Add photo thumbnail path to export_data for printable assets
|
|
if asset_type == "PRINTABLE" and props.photo_thumbnail:
|
|
export_data["photo_thumbnail_path"] = bpy.path.abspath(
|
|
props.photo_thumbnail
|
|
)
|
|
|
|
eval_path_computing = (
|
|
"bpy.data.objects['%s'].blenderkit.uploading" % mainmodel.name
|
|
)
|
|
eval_path_state = (
|
|
"bpy.data.objects['%s'].blenderkit.upload_state" % mainmodel.name
|
|
)
|
|
eval_path = "bpy.data.objects['%s']" % mainmodel.name
|
|
|
|
upload_data = {
|
|
"assetType": asset_type.lower(),
|
|
}
|
|
|
|
# Common parameters for both MODEL and PRINTABLE
|
|
upload_params = {
|
|
"faceCount": props.face_count,
|
|
"modifiers": utils.string2list(props.modifiers),
|
|
"dimensionX": round(props.dimensions[0], 4),
|
|
"dimensionY": round(props.dimensions[1], 4),
|
|
"dimensionZ": round(props.dimensions[2], 4),
|
|
"boundBoxMinX": round(props.bbox_min[0], 4),
|
|
"boundBoxMinY": round(props.bbox_min[1], 4),
|
|
"boundBoxMinZ": round(props.bbox_min[2], 4),
|
|
"boundBoxMaxX": round(props.bbox_max[0], 4),
|
|
"boundBoxMaxY": round(props.bbox_max[1], 4),
|
|
"boundBoxMaxZ": round(props.bbox_max[2], 4),
|
|
}
|
|
|
|
# Additional parameters only for MODEL type
|
|
if asset_type == "MODEL":
|
|
engines = [props.engine.lower()]
|
|
if props.engine1 != "NONE":
|
|
engines.append(props.engine1.lower())
|
|
if props.engine2 != "NONE":
|
|
engines.append(props.engine2.lower())
|
|
if props.engine3 != "NONE":
|
|
engines.append(props.engine3.lower())
|
|
if props.engine == "OTHER":
|
|
engines.append(props.engine_other.lower())
|
|
|
|
style = props.style.lower()
|
|
|
|
upload_params.update(
|
|
{
|
|
"productionLevel": props.production_level.lower(),
|
|
"modelStyle": style,
|
|
"engines": engines,
|
|
"materials": utils.string2list(props.materials),
|
|
"shaders": utils.string2list(props.shaders),
|
|
"uv": props.uv,
|
|
"animated": props.animated,
|
|
"rig": props.rig,
|
|
"simulation": props.simulation,
|
|
"purePbr": props.pbr,
|
|
"faceCountRender": props.face_count_render,
|
|
"manifold": props.manifold,
|
|
"objectCount": props.object_count,
|
|
"procedural": props.is_procedural,
|
|
"nodeCount": props.node_count,
|
|
"textureCount": props.texture_count,
|
|
"megapixels": props.total_megapixels,
|
|
}
|
|
)
|
|
|
|
if props.use_design_year:
|
|
upload_params["designYear"] = props.design_year
|
|
if props.condition != "UNSPECIFIED":
|
|
upload_params["condition"] = props.condition.lower()
|
|
if props.pbr:
|
|
pt = props.pbr_type
|
|
pt = pt.lower()
|
|
upload_params["pbrType"] = pt
|
|
if props.texture_resolution_max > 0:
|
|
upload_params["textureResolutionMax"] = props.texture_resolution_max
|
|
upload_params["textureResolutionMin"] = props.texture_resolution_min
|
|
if props.mesh_poly_type != "OTHER":
|
|
upload_params["meshPolyType"] = props.mesh_poly_type.lower()
|
|
|
|
# Common optional parameters for both MODEL and PRINTABLE
|
|
optional_params = [
|
|
"manufacturer",
|
|
"designer",
|
|
"design_collection",
|
|
"design_variant",
|
|
]
|
|
for p in optional_params:
|
|
if eval("props.%s" % p) != "":
|
|
upload_params[sub_to_camel(p)] = eval("props.%s" % p)
|
|
|
|
if props.use_design_year:
|
|
upload_params["designYear"] = props.design_year
|
|
|
|
if props.sexualized_content:
|
|
upload_params["sexualizedContent"] = props.sexualized_content
|
|
|
|
elif asset_type == "SCENE":
|
|
# Prepare to save the file
|
|
s = bpy.context.scene
|
|
|
|
props = s.blenderkit
|
|
|
|
export_data["scene"] = s.name
|
|
export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
|
|
|
|
eval_path_computing = "bpy.data.scenes['%s'].blenderkit.uploading" % s.name
|
|
eval_path_state = "bpy.data.scenes['%s'].blenderkit.upload_state" % s.name
|
|
eval_path = "bpy.data.scenes['%s']" % s.name
|
|
|
|
engines = [props.engine.lower()]
|
|
if props.engine1 != "NONE":
|
|
engines.append(props.engine1.lower())
|
|
if props.engine2 != "NONE":
|
|
engines.append(props.engine2.lower())
|
|
if props.engine3 != "NONE":
|
|
engines.append(props.engine3.lower())
|
|
if props.engine == "OTHER":
|
|
engines.append(props.engine_other.lower())
|
|
|
|
style = props.style.lower()
|
|
# if style == 'OTHER':
|
|
# style = props.style_other.lower()
|
|
|
|
upload_data = {
|
|
"assetType": "scene",
|
|
}
|
|
upload_params = {
|
|
"productionLevel": props.production_level.lower(),
|
|
"modelStyle": style,
|
|
"engines": engines,
|
|
"modifiers": utils.string2list(props.modifiers),
|
|
"materials": utils.string2list(props.materials),
|
|
"shaders": utils.string2list(props.shaders),
|
|
"uv": props.uv,
|
|
"animated": props.animated,
|
|
# "simulation": props.simulation,
|
|
"purePbr": props.pbr,
|
|
"faceCount": 1, # props.face_count,
|
|
"faceCountRender": 1, # props.face_count_render,
|
|
"objectCount": 1, # props.object_count,
|
|
# "scene": props.is_scene,
|
|
}
|
|
if props.use_design_year:
|
|
upload_params["designYear"] = props.design_year
|
|
if props.condition != "UNSPECIFIED":
|
|
upload_params["condition"] = props.condition.lower()
|
|
if props.pbr:
|
|
pt = props.pbr_type
|
|
pt = pt.lower()
|
|
upload_params["pbrType"] = pt
|
|
|
|
if props.texture_resolution_max > 0:
|
|
upload_params["textureResolutionMax"] = props.texture_resolution_max
|
|
upload_params["textureResolutionMin"] = props.texture_resolution_min
|
|
if props.mesh_poly_type != "OTHER":
|
|
upload_params["meshPolyType"] = (
|
|
props.mesh_poly_type.lower()
|
|
) # .replace('_',' ')
|
|
|
|
elif asset_type == "MATERIAL":
|
|
mat = bpy.context.active_object.active_material
|
|
props = mat.blenderkit
|
|
|
|
# props.name = mat.name
|
|
|
|
export_data["material"] = str(mat.name)
|
|
export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
|
|
# mat analytics happen here, since they don't take up any time...
|
|
asset_inspector.check_material(props, mat)
|
|
|
|
eval_path_computing = "bpy.data.materials['%s'].blenderkit.uploading" % mat.name
|
|
eval_path_state = "bpy.data.materials['%s'].blenderkit.upload_state" % mat.name
|
|
eval_path = "bpy.data.materials['%s']" % mat.name
|
|
|
|
engine = props.engine
|
|
if engine == "OTHER":
|
|
engine = props.engine_other
|
|
engine = engine.lower()
|
|
style = props.style.lower()
|
|
# if style == 'OTHER':
|
|
# style = props.style_other.lower()
|
|
|
|
upload_data = {
|
|
"assetType": "material",
|
|
}
|
|
|
|
upload_params = {
|
|
"materialStyle": style,
|
|
"engine": engine,
|
|
"shaders": utils.string2list(props.shaders),
|
|
"uv": props.uv,
|
|
"animated": props.animated,
|
|
"purePbr": props.pbr,
|
|
"textureSizeMeters": props.texture_size_meters,
|
|
"procedural": props.is_procedural,
|
|
"nodeCount": props.node_count,
|
|
"textureCount": props.texture_count,
|
|
"megapixels": props.total_megapixels,
|
|
}
|
|
|
|
if props.pbr:
|
|
upload_params["pbrType"] = props.pbr_type.lower()
|
|
|
|
if props.texture_resolution_max > 0:
|
|
upload_params["textureResolutionMax"] = props.texture_resolution_max
|
|
upload_params["textureResolutionMin"] = props.texture_resolution_min
|
|
|
|
elif asset_type == "BRUSH":
|
|
brush = utils.get_active_brush()
|
|
|
|
props = brush.blenderkit
|
|
# props.name = brush.name
|
|
|
|
export_data["brush"] = str(brush.name)
|
|
export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath)
|
|
|
|
eval_path_computing = "bpy.data.brushes['%s'].blenderkit.uploading" % brush.name
|
|
eval_path_state = "bpy.data.brushes['%s'].blenderkit.upload_state" % brush.name
|
|
eval_path = "bpy.data.brushes['%s']" % brush.name
|
|
|
|
# mat analytics happen here, since they don't take up any time...
|
|
|
|
brush_type = ""
|
|
if bpy.context.sculpt_object is not None:
|
|
brush_type = "sculpt"
|
|
|
|
elif bpy.context.image_paint_object: # could be just else, but for future p
|
|
brush_type = "texture_paint"
|
|
|
|
upload_params = {
|
|
"mode": brush_type,
|
|
}
|
|
|
|
upload_data = {
|
|
"assetType": "brush",
|
|
}
|
|
|
|
elif asset_type == "HDR":
|
|
ui_props = bpy.context.window_manager.blenderkitUI
|
|
|
|
# imagename = ui_props.hdr_upload_image
|
|
image = ui_props.hdr_upload_image # bpy.data.images.get(imagename)
|
|
if not image:
|
|
return None, None
|
|
|
|
props = image.blenderkit
|
|
|
|
image_utils.analyze_image_is_true_hdr(image)
|
|
|
|
# props.name = brush.name
|
|
base, ext = os.path.splitext(image.filepath)
|
|
thumb_path = base + ".jpg"
|
|
export_data["thumbnail_path"] = bpy.path.abspath(thumb_path)
|
|
|
|
export_data["hdr"] = str(image.name)
|
|
export_data["hdr_filepath"] = str(bpy.path.abspath(image.filepath))
|
|
# export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath)
|
|
|
|
eval_path_computing = "bpy.data.images['%s'].blenderkit.uploading" % image.name
|
|
eval_path_state = "bpy.data.images['%s'].blenderkit.upload_state" % image.name
|
|
eval_path = "bpy.data.images['%s']" % image.name
|
|
|
|
# mat analytics happen here, since they don't take up any time...
|
|
|
|
upload_params = {
|
|
"textureResolutionMax": props.texture_resolution_max,
|
|
"trueHDR": props.true_hdr,
|
|
}
|
|
|
|
upload_data = {
|
|
"assetType": "hdr",
|
|
}
|
|
|
|
elif asset_type == "TEXTURE":
|
|
style = props.style
|
|
# if style == 'OTHER':
|
|
# style = props.style_other
|
|
|
|
upload_data = {
|
|
"assetType": "texture",
|
|
}
|
|
upload_params = {
|
|
"style": style,
|
|
"animated": props.animated,
|
|
"purePbr": props.pbr,
|
|
"resolution": props.resolution,
|
|
}
|
|
if props.pbr:
|
|
pt = props.pbr_type
|
|
pt = pt.lower()
|
|
upload_data["pbrType"] = pt
|
|
|
|
elif asset_type == "NODEGROUP":
|
|
ui_props = bpy.context.window_manager.blenderkitUI
|
|
bk_logger.info("preparing nodegroup upload")
|
|
asset = ui_props.nodegroup_upload
|
|
bk_logger.info("asset:" + str(asset))
|
|
if not asset:
|
|
return None, None
|
|
|
|
props = asset.blenderkit
|
|
|
|
export_data["nodegroup"] = str(asset.name)
|
|
export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
|
|
eval_path_computing = (
|
|
f"bpy.data.node_groups['{asset.name}'].blenderkit.uploading"
|
|
)
|
|
eval_path_state = (
|
|
f"bpy.data.node_groups['{asset.name}'].blenderkit.upload_state"
|
|
)
|
|
eval_path = f"bpy.data.node_groups['{asset.name}']"
|
|
|
|
# mat analytics happen here, since they don't take up any time...
|
|
|
|
upload_params = {"nodeType": asset.type.lower()}
|
|
|
|
upload_data = {
|
|
"assetType": "nodegroup",
|
|
}
|
|
add_version(upload_data)
|
|
|
|
# caller can be upload operator, but also asset bar called from tooltip generator
|
|
if caller and caller.properties.main_file is True:
|
|
upload_data["name"] = props.name
|
|
upload_data["displayName"] = props.name
|
|
else:
|
|
upload_data["displayName"] = props.name
|
|
|
|
upload_data["description"] = props.description
|
|
upload_data["tags"] = utils.string2list(props.tags)
|
|
# category is always only one value by a slug, that's why we go down to the lowest level and overwrite.
|
|
if props.category == "" or props.category == "NONE":
|
|
upload_data["category"] = asset_type.lower()
|
|
else:
|
|
upload_data["category"] = props.category
|
|
if props.subcategory not in (
|
|
"NONE",
|
|
"EMPTY",
|
|
"OTHER",
|
|
): # if OTHER category is selected, parent category will be used
|
|
upload_data["category"] = props.subcategory
|
|
if props.subcategory1 not in (
|
|
"NONE",
|
|
"EMPTY",
|
|
"OTHER",
|
|
): # if OTHER category is selected, parent category will be used
|
|
upload_data["category"] = props.subcategory1
|
|
|
|
upload_data["license"] = props.license
|
|
upload_data["isFree"] = props.is_free == "FREE"
|
|
upload_data["isPrivate"] = props.is_private == "PRIVATE"
|
|
upload_data["token"] = user_preferences.api_key
|
|
|
|
upload_data["parameters"] = upload_params
|
|
|
|
# if props.asset_base_id != '':
|
|
export_data["assetBaseId"] = props.asset_base_id
|
|
export_data["id"] = props.id
|
|
export_data["eval_path_computing"] = eval_path_computing
|
|
export_data["eval_path_state"] = eval_path_state
|
|
export_data["eval_path"] = eval_path
|
|
bk_logger.info("export_data:" + str(export_data))
|
|
|
|
return export_data, upload_data
|
|
|
|
|
|
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.",
|
|
)
|
|
|
|
|
|
def can_edit_asset(active_index: int = -1, asset_data: Optional[dict] = None):
|
|
if active_index < 0 and not asset_data:
|
|
return False
|
|
profile = global_vars.BKIT_PROFILE
|
|
if profile is None:
|
|
return False
|
|
if utils.profile_is_validator():
|
|
return True
|
|
if not asset_data:
|
|
sr = search.get_search_results()
|
|
asset_data = dict(sr[active_index])
|
|
if int(asset_data["author"]["id"]) == profile.id:
|
|
return True
|
|
return False
|
|
|
|
|
|
class FastMetadata(bpy.types.Operator):
|
|
"""Edit metadata of the asset"""
|
|
|
|
bl_idname = "wm.blenderkit_fast_metadata"
|
|
bl_label = "Update metadata"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
asset_id: StringProperty( # type: ignore[valid-type]
|
|
name="Asset Base Id",
|
|
description="Unique name of the asset (hidden)",
|
|
default="",
|
|
)
|
|
asset_type: StringProperty(name="Asset Type", description="Asset Type", default="") # type: ignore[valid-type]
|
|
name: StringProperty( # type: ignore[valid-type]
|
|
name="Name",
|
|
description="Provide name of your asset, choose a few descriptive English words that clearly identify and distinguish your asset. Good naming helps your asset to be found on the search engine. Follow these tips:\n\n"
|
|
"Use Descriptive Terms:\nInclude specific details such as the brand, material, or distinct features of the asset.\n\n"
|
|
"Avoid Generic or Vague Terms:\nNames like 'Sky 01' or 'Big Tree' are too general and not helpful for search optimization. Instead, use names that provide meaningful information about the asset.\n\n"
|
|
"Highlight Key Attributes:\nIncorporate important attributes that are likely to be used in search queries, such as the model in vehicles or the designer in furniture.\n\n"
|
|
"Bad names: Chair 01, Nice Car, Statue\n"
|
|
"Good names: Knoll Steel Chair, Skoda Kodiaq, Statue of Liberty",
|
|
default="",
|
|
)
|
|
description: StringProperty( # type: ignore[valid-type]
|
|
name="Description",
|
|
description="Provide a clear and concise description of your asset in English. To enhance searchability and discoverability of your asset, follow these tips:\n\n"
|
|
"Be Specific:\nUse precise terms that accurately reflect the asset. Include key characteristics such as material, color, function, or designer/brand.\n\n"
|
|
"Use Recognizable Keywords:\nIncorporate well-known and relevant keywords that users are likely to search for. This includes brand names, designer names, common usage, and industry-specific terms.\n\n"
|
|
"Avoid Jargon:\nUnless industry-specific terms are widely understood by your target audience, use simple language that is easy to understand.\n\n"
|
|
"Highlight Unique Features:\nMention any distinctive features that set the asset apart from others, such as a unique design, special function, or notable collaboration.\n\n"
|
|
"Keep it Brief:\nAim for a short description that captures the essence of the asset without unnecessary details. A concise description makes it easier for Elasticsearch to process and for users to scan",
|
|
default="",
|
|
)
|
|
tags: StringProperty( # type: ignore[valid-type]
|
|
name="Tags",
|
|
description="Enter up to 10 tags, separated by commas. Tags may include alphanumeric characters and underscores only. For better discoverability, follow these tips:\n\n"
|
|
"Choose Relevant Keywords:\nSelect tags that closely relate to the asset's features, usage, or industry terms. This increases the chances that your asset appears in relevant searches.\n\n"
|
|
"Include Synonyms:\nAdd variations or synonyms to cover different ways users might search for similar items. Especially consider synonyms for terms used in the asset's name or description to broaden search relevancy.\n\n"
|
|
"Prioritize Common Terms:\nUse commonly searched terms within your target audience. This helps connect your assets to the most likely queries.\n\n"
|
|
"Enhance with Specificity: While common terms are essential, adding specific tags can help in uniquely identifying and categorizing the asset. This is particularly useful for users looking for particular features or attributes.",
|
|
default="",
|
|
)
|
|
category: EnumProperty( # type: ignore[valid-type]
|
|
name="Category",
|
|
description="Select the main category for the uploaded asset. "
|
|
"Choose the most accurate category to enhance visibility and download rates. "
|
|
"Proper categorization ensures your asset reaches people actively searching for assets like yours",
|
|
items=categories.get_category_enums,
|
|
update=categories.update_category_enums,
|
|
)
|
|
subcategory: EnumProperty( # type: ignore[valid-type]
|
|
name="Subcategory",
|
|
description="Select a subcategory within the chosen main category",
|
|
items=categories.get_subcategory_enums,
|
|
update=categories.update_subcategory_enums,
|
|
)
|
|
subcategory1: EnumProperty( # type: ignore[valid-type]
|
|
name="Sub-subcategory",
|
|
description="Select a further subcategory within the chosen subcategory",
|
|
items=categories.get_subcategory1_enums,
|
|
)
|
|
license: EnumProperty( # type: ignore[valid-type]
|
|
items=licenses,
|
|
default="royalty_free",
|
|
description="License. Please read our help for choosing the right licenses",
|
|
)
|
|
is_private: EnumProperty( # type: ignore[valid-type]
|
|
name="Thumbnail Style",
|
|
items=(
|
|
(
|
|
"PRIVATE",
|
|
"Private",
|
|
"You asset will be hidden to public. The private assets are limited by a quota.",
|
|
),
|
|
(
|
|
"PUBLIC",
|
|
"Public",
|
|
'"Your asset will go into the validation process automatically',
|
|
),
|
|
),
|
|
description="If not marked private, your asset will go into the validation process automatically\n"
|
|
"Private assets are limited by quota",
|
|
default="PUBLIC",
|
|
)
|
|
sexualized_content: BoolProperty( # type: ignore[valid-type]
|
|
name="Sexualized content",
|
|
description=(
|
|
"Flag this asset if it includes explicit content, suggestive poses, or overemphasized secondary sexual characteristics. "
|
|
"This helps users filter content according to their preferences, creating a safe and inclusive browsing experience for all.\n\n"
|
|
"Flag not required:\n"
|
|
"- naked base mesh model,\n"
|
|
"- figure in underwear/swimwear in neutral position.\n\n"
|
|
"Flag required:\n"
|
|
"- figure in sexually suggestive pose,\n"
|
|
"- figure with over overemphasized sexual characteristics,\n"
|
|
"- objects related to sexual act."
|
|
),
|
|
default=False,
|
|
)
|
|
free_full: EnumProperty( # type: ignore[valid-type]
|
|
name="Free or Full Plan",
|
|
items=(
|
|
(
|
|
"FREE",
|
|
"Free",
|
|
"You consent you want to release this asset as free for everyone",
|
|
),
|
|
("FULL", "Full", "Your asset will be in the full plan"),
|
|
),
|
|
description="Choose whether the asset should be free or in the Full Plan",
|
|
default="FULL",
|
|
update=update_free_full,
|
|
)
|
|
|
|
####################
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
# col = layout.column()
|
|
layout.label(text=self.message)
|
|
layout.prop(self, "category")
|
|
if self.category != "NONE" and self.subcategory != "EMPTY":
|
|
layout.prop(self, "subcategory")
|
|
if self.subcategory != "NONE" and self.subcategory1 != "EMPTY":
|
|
layout.prop(self, "subcategory1")
|
|
layout.prop(self, "name")
|
|
layout.prop(self, "description")
|
|
layout.prop(self, "tags")
|
|
layout.prop(self, "is_private", expand=True)
|
|
layout.prop(self, "free_full", expand=True)
|
|
if self.is_private == "PUBLIC":
|
|
layout.prop(self, "license")
|
|
# layout.label(text="Content Flags:")
|
|
content_flag_box = layout.box()
|
|
content_flag_box.alignment = "EXPAND"
|
|
content_flag_box.label(text="Sensitive Content Flags:")
|
|
content_flag_box.prop(self, "sexualized_content")
|
|
|
|
def execute(self, context):
|
|
if self.subcategory1 not in ("NONE", "EMPTY"):
|
|
category = self.subcategory1
|
|
elif self.subcategory not in ("NONE", "EMPTY"):
|
|
category = self.subcategory
|
|
else:
|
|
category = self.category
|
|
utils.update_tags(self, context)
|
|
metadata = {
|
|
"category": category,
|
|
"displayName": self.name,
|
|
"description": self.description,
|
|
"tags": utils.string2list(self.tags),
|
|
"isPrivate": self.is_private == "PRIVATE",
|
|
"isFree": self.free_full == "FREE",
|
|
"license": self.license,
|
|
"parameters": [
|
|
{
|
|
"parameterType": "sexualizedContent",
|
|
"value": self.sexualized_content,
|
|
},
|
|
],
|
|
}
|
|
url = f"{paths.BLENDERKIT_API}/assets/{self.asset_id}/"
|
|
messages = {
|
|
"success": "Metadata upload succeded",
|
|
"error": "Metadata upload failed",
|
|
}
|
|
client_lib.nonblocking_request(url, "PATCH", {}, metadata, messages)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
ui_props = bpy.context.window_manager.blenderkitUI
|
|
if ui_props.active_index > -1:
|
|
sr = search.get_search_results()
|
|
asset_data = dict(sr[ui_props.active_index])
|
|
else:
|
|
active_asset = utils.get_active_asset_by_type(asset_type=self.asset_type)
|
|
asset_data = active_asset.get("asset_data")
|
|
|
|
if not can_edit_asset(asset_data=asset_data):
|
|
return {"CANCELLED"}
|
|
self.asset_id = asset_data["id"]
|
|
self.asset_type = asset_data["assetType"]
|
|
cat_path = categories.get_category_path(
|
|
global_vars.DATA["bkit_categories"], asset_data["category"]
|
|
)
|
|
try:
|
|
if len(cat_path) > 1:
|
|
self.category = cat_path[1]
|
|
if len(cat_path) > 2:
|
|
self.subcategory = cat_path[2]
|
|
except Exception as e:
|
|
bk_logger.error(e)
|
|
|
|
self.message = f"Fast edit metadata of {asset_data['displayName']}"
|
|
self.name = asset_data["displayName"]
|
|
self.description = asset_data["description"]
|
|
self.tags = ",".join(asset_data["tags"])
|
|
if asset_data["isPrivate"]:
|
|
self.is_private = "PRIVATE"
|
|
else:
|
|
self.is_private = "PUBLIC"
|
|
|
|
if asset_data["isFree"]:
|
|
self.free_full = "FREE"
|
|
else:
|
|
self.free_full = "FULL"
|
|
self.license = asset_data["license"]
|
|
self.sexualized_content = asset_data.get("dictParameters", {}).get(
|
|
"sexualizedContent", False
|
|
)
|
|
|
|
wm = context.window_manager
|
|
|
|
return wm.invoke_props_dialog(self, width=600)
|
|
|
|
|
|
def get_upload_location(props):
|
|
"""
|
|
not used by now, gets location of uploaded asset - potentially usefull if we draw a nice upload gizmo in viewport.
|
|
Parameters
|
|
----------
|
|
props
|
|
|
|
Returns
|
|
-------
|
|
|
|
"""
|
|
ui_props = bpy.context.window_manager.blenderkitUI
|
|
if ui_props.asset_type == "MODEL":
|
|
if bpy.context.view_layer.objects.active is not None:
|
|
ob = utils.get_active_model()
|
|
return ob.location
|
|
if ui_props.asset_type == "SCENE":
|
|
return None
|
|
elif ui_props.asset_type == "MATERIAL":
|
|
if (
|
|
bpy.context.view_layer.objects.active is not None
|
|
and bpy.context.active_object.active_material is not None
|
|
):
|
|
return bpy.context.active_object.location
|
|
elif ui_props.asset_type == "TEXTURE":
|
|
return None
|
|
elif ui_props.asset_type == "BRUSH":
|
|
return None
|
|
return None
|
|
|
|
|
|
def storage_quota_available(props) -> bool:
|
|
"""Check the storage quota if there is available space to upload."""
|
|
profile = global_vars.BKIT_PROFILE
|
|
if profile is None:
|
|
props.report = "Please log-in first."
|
|
return False
|
|
|
|
if props.is_private == "PUBLIC":
|
|
return True
|
|
|
|
if profile.remainingPrivateQuota is not None and profile.remainingPrivateQuota > 0:
|
|
return True
|
|
|
|
props.report = "Private storage quota exceeded."
|
|
return False
|
|
|
|
|
|
def auto_fix(asset_type=""):
|
|
# this applies various procedures to ensure coherency in the database.
|
|
asset = utils.get_active_asset()
|
|
props = utils.get_upload_props()
|
|
if asset_type == "MATERIAL":
|
|
overrides.ensure_eevee_transparency(asset)
|
|
asset.name = props.name
|
|
|
|
|
|
def prepare_asset_data(self, context, asset_type, reupload, upload_set):
|
|
"""Process asset and its data for upload."""
|
|
props = utils.get_upload_props()
|
|
utils.name_update(props) # fix the name first
|
|
|
|
if storage_quota_available(props) is False:
|
|
self.report({"ERROR_INVALID_INPUT"}, props.report)
|
|
return False, None, None
|
|
|
|
auto_fix(asset_type=asset_type)
|
|
|
|
# do this for fixing long tags in some upload cases
|
|
props.tags = props.tags[:]
|
|
|
|
# check for missing metadata
|
|
|
|
check_missing_data(asset_type, props, upload_set=upload_set)
|
|
# if previous check did find any problems then
|
|
if props.report != "":
|
|
return False, None, None
|
|
|
|
if not reupload:
|
|
props.asset_base_id = ""
|
|
props.id = ""
|
|
|
|
export_data, upload_data = get_upload_data(
|
|
caller=self, context=context, asset_type=asset_type
|
|
)
|
|
|
|
# check if thumbnail exists, generate for HDR:
|
|
if "THUMBNAIL" in upload_set:
|
|
if asset_type == "HDR":
|
|
image_utils.generate_hdr_thumbnail()
|
|
# get upload data because the image utils function sets true_hdr
|
|
export_data, upload_data = get_upload_data(
|
|
caller=self, context=context, asset_type=asset_type
|
|
)
|
|
|
|
elif not os.path.exists(export_data["thumbnail_path"]):
|
|
props.upload_state = "0% - thumbnail not found"
|
|
props.uploading = False
|
|
return False, None, None
|
|
|
|
# Check if photo thumbnail exists for printable assets when it's included in upload_set
|
|
if "photo_thumbnail" in upload_set:
|
|
if asset_type == "PRINTABLE" and "photo_thumbnail_path" in export_data:
|
|
if not os.path.exists(export_data["photo_thumbnail_path"]):
|
|
props.upload_state = "0% - photo thumbnail not found"
|
|
props.uploading = False
|
|
return False, None, None
|
|
|
|
# save a copy of the file for processing. Only for blend files
|
|
_, ext = os.path.splitext(bpy.data.filepath)
|
|
if not ext:
|
|
ext = ".blend"
|
|
export_data["temp_dir"] = tempfile.mkdtemp()
|
|
export_data["source_filepath"] = os.path.join(
|
|
export_data["temp_dir"], "export_blenderkit" + ext
|
|
)
|
|
if asset_type != "HDR":
|
|
# if this isn't here, blender crashes.
|
|
if bpy.app.version >= (3, 0, 0):
|
|
bpy.context.preferences.filepaths.file_preview_type = "NONE"
|
|
|
|
bpy.ops.wm.save_as_mainfile(
|
|
filepath=export_data["source_filepath"], compress=False, copy=True
|
|
)
|
|
|
|
export_data["binary_path"] = bpy.app.binary_path
|
|
export_data["debug_value"] = bpy.app.debug_value
|
|
|
|
return True, upload_data, export_data
|
|
|
|
|
|
asset_types = (
|
|
("MODEL", "Model", "Set of objects"),
|
|
("SCENE", "Scene", "Scene"),
|
|
("HDR", "HDR", "HDR image"),
|
|
("MATERIAL", "Material", "Any .blend Material"),
|
|
("TEXTURE", "Texture", "A texture, or texture set"),
|
|
("BRUSH", "Brush", "Brush, can be any type of blender brush"),
|
|
("NODEGROUP", "Tool", "Geometry nodes tool"),
|
|
("PRINTABLE", "Printable", "3D printable model"),
|
|
("ADDON", "Addon", "Addon"),
|
|
)
|
|
|
|
|
|
class UploadOperator(Operator):
|
|
"""Tooltip"""
|
|
|
|
bl_idname = "object.blenderkit_upload"
|
|
bl_description = "Upload or re-upload asset + thumbnail + metadata"
|
|
|
|
bl_label = "BlenderKit Asset Upload"
|
|
bl_options = {"REGISTER", "INTERNAL"}
|
|
|
|
# type of upload - model, material, textures, e.t.c.
|
|
asset_type: EnumProperty( # type: ignore[valid-type]
|
|
name="Type",
|
|
items=asset_types,
|
|
description="Type of upload",
|
|
default="MODEL",
|
|
)
|
|
|
|
reupload: BoolProperty( # type: ignore[valid-type]
|
|
name="reupload",
|
|
description="reupload but also draw so that it asks what to reupload",
|
|
default=False,
|
|
options={"SKIP_SAVE"},
|
|
)
|
|
|
|
metadata: BoolProperty(name="metadata", default=True, options={"SKIP_SAVE"}) # type: ignore[valid-type]
|
|
|
|
thumbnail: BoolProperty(name="thumbnail", default=False, options={"SKIP_SAVE"}) # type: ignore[valid-type]
|
|
|
|
# Add new property for photo thumbnail
|
|
photo_thumbnail: BoolProperty(name="photo thumbnail", default=False, options={"SKIP_SAVE"}) # type: ignore[valid-type]
|
|
|
|
main_file: BoolProperty(name="main file", default=False, options={"SKIP_SAVE"}) # type: ignore[valid-type]
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return utils.uploadable_asset_poll()
|
|
|
|
def execute(self, context):
|
|
bpy.ops.object.blenderkit_auto_tags()
|
|
props = utils.get_upload_props()
|
|
|
|
upload_set = []
|
|
if not self.reupload:
|
|
upload_set = ["METADATA", "THUMBNAIL", "MAINFILE"]
|
|
# Add photo_thumbnail to the upload set for printable assets
|
|
if self.asset_type == "PRINTABLE" and props.photo_thumbnail:
|
|
upload_set.append("photo_thumbnail")
|
|
else:
|
|
if self.metadata:
|
|
upload_set.append("METADATA")
|
|
if self.thumbnail:
|
|
upload_set.append("THUMBNAIL")
|
|
if self.photo_thumbnail:
|
|
upload_set.append("photo_thumbnail")
|
|
if self.main_file:
|
|
upload_set.append("MAINFILE")
|
|
|
|
# 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
|
|
|
|
ok, upload_data, export_data = prepare_asset_data(
|
|
self, context, self.asset_type, self.reupload, upload_set=upload_set
|
|
)
|
|
if not ok:
|
|
self.report({"ERROR_INVALID_INPUT"}, props.report)
|
|
props.upload_state = ""
|
|
return {"CANCELLED"}
|
|
|
|
props.upload_state = "Upload initiating..."
|
|
props.uploading = True
|
|
|
|
client_lib.asset_upload(upload_data, export_data, upload_set)
|
|
return {"FINISHED"}
|
|
|
|
def draw(self, context):
|
|
props = utils.get_upload_props()
|
|
layout = self.layout
|
|
|
|
if self.reupload:
|
|
utils.label_multiline(
|
|
layout,
|
|
text="To update only metadata of the model, keep checkboxes unchecked",
|
|
width=500,
|
|
)
|
|
# layout.prop(self, 'metadata')
|
|
layout.prop(self, "main_file")
|
|
layout.prop(self, "thumbnail")
|
|
|
|
# Show photo_thumbnail option only for printable assets
|
|
if self.asset_type == "PRINTABLE":
|
|
layout.prop(self, "photo_thumbnail")
|
|
|
|
if props.asset_base_id != "" and not self.reupload:
|
|
utils.label_multiline(
|
|
layout,
|
|
text="Really upload as new?\n\n"
|
|
"Do this only when you create a new asset from an old one.\n"
|
|
"For updates of thumbnail or model use reupload.\n",
|
|
width=400,
|
|
icon="ERROR",
|
|
)
|
|
|
|
if props.is_private == "PUBLIC":
|
|
if self.asset_type == "MODEL":
|
|
utils.label_multiline(
|
|
layout,
|
|
text="\nYou marked the asset as public. "
|
|
"This means it will be validated by our team.\n\n"
|
|
"Please test your upload after it finishes:\n"
|
|
"- Open a new file\n"
|
|
"- Find the asset and download it\n"
|
|
"- Check if it snaps correctly to surfaces\n"
|
|
"- Check if it has all textures and renders as expected\n"
|
|
"- Check if it has correct size in world units (for models)",
|
|
width=400,
|
|
)
|
|
elif self.asset_type == "HDR":
|
|
if not props.true_hdr:
|
|
utils.label_multiline(
|
|
layout,
|
|
text="This image isn't HDR,\n"
|
|
"It has a low dynamic range.\n"
|
|
"BlenderKit library accepts 360 degree images\n"
|
|
"however the default filter setting for search\n"
|
|
"is to show only true HDR images\n",
|
|
icon="ERROR",
|
|
width=500,
|
|
)
|
|
|
|
utils.label_multiline(
|
|
layout,
|
|
text="You marked the asset as public. "
|
|
"This means it will be validated by our team.\n\n"
|
|
"Please test your upload after it finishes:\n"
|
|
"- Open a new file\n"
|
|
"- Find the asset and download it\n"
|
|
"- Check if it works as expected\n",
|
|
width=500,
|
|
)
|
|
else:
|
|
utils.label_multiline(
|
|
layout,
|
|
text="You marked the asset as public."
|
|
"This means it will be validated by our team.\n\n"
|
|
"Please test your upload after it finishes:\n"
|
|
"- Open a new file\n"
|
|
"- Find the asset and download it\n"
|
|
"- Check if it works as expected\n",
|
|
width=500,
|
|
)
|
|
|
|
if props.is_private == "PRIVATE":
|
|
utils.label_multiline(
|
|
layout,
|
|
width=500,
|
|
text="Would you like tu upload your asset to BlenderKit?",
|
|
)
|
|
|
|
def invoke(self, context, event):
|
|
if not utils.user_logged_in():
|
|
ui_panels.draw_not_logged_in(
|
|
self, message="To upload assets you need to login/signup."
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
if self.asset_type == "HDR":
|
|
# getting upload data for images ensures true_hdr check so users can be informed about their handling
|
|
# simple 360 photos or renders with LDR are hidden by default..
|
|
export_data, upload_data = get_upload_data(asset_type="HDR")
|
|
|
|
# if props.is_private == 'PUBLIC':
|
|
return context.window_manager.invoke_props_dialog(self, width=500)
|
|
# else:
|
|
# return self.execute(context)
|
|
|
|
|
|
class AssetDebugPrint(Operator):
|
|
"""Change verification status"""
|
|
|
|
bl_idname = "object.blenderkit_print_asset_debug"
|
|
bl_description = "BlenderKit print asset data for debug purposes"
|
|
bl_label = "BlenderKit print asset data"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
# type of upload - model, material, textures, e.t.c.
|
|
asset_id: StringProperty( # type: ignore[valid-type]
|
|
name="asset id",
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
|
|
def execute(self, context):
|
|
if not search.get_search_results():
|
|
print("no search results found")
|
|
return {"CANCELLED"}
|
|
# update status in search results for validator's clarity
|
|
sr = search.get_search_results()
|
|
|
|
result = None
|
|
for r in sr:
|
|
if r["id"] == self.asset_id:
|
|
result = r
|
|
if not result:
|
|
ad = bpy.context.active_object.get("asset_data")
|
|
if ad:
|
|
result = ad.to_dict()
|
|
if result:
|
|
t = bpy.data.texts.new(result["displayName"])
|
|
t.write(json.dumps(result, indent=4, sort_keys=True))
|
|
print(json.dumps(result, indent=4, sort_keys=True))
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AssetVerificationStatusChange(Operator):
|
|
"""Change verification status"""
|
|
|
|
bl_idname = "object.blenderkit_change_status"
|
|
bl_description = "Change asset status"
|
|
bl_label = "Change verification status"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
# type of upload - model, material, textures, e.t.c.
|
|
asset_id: StringProperty( # type: ignore[valid-type]
|
|
name="asset id",
|
|
)
|
|
|
|
state: StringProperty(name="verification_status", default="uploaded") # type: ignore[valid-type]
|
|
|
|
original_state: StringProperty(name="verification_status", default="uploaded") # type: ignore[valid-type]
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
# if self.state == 'deleted':
|
|
message = "Really delete asset from BlenderKit online storage?"
|
|
if self.original_state == "on_hold":
|
|
message += (
|
|
"\n\nThis asset is on hold. If you want to upload it again,"
|
|
" please reupload the asset instead of deleting it and "
|
|
"uploading it as a new one. "
|
|
"This will preserve the validation history in the comments and avoid any misunderstandings."
|
|
)
|
|
utils.label_multiline(layout, text=message, width=300)
|
|
|
|
# layout.prop(self, 'state')
|
|
|
|
def execute(self, context):
|
|
if not search.get_search_results():
|
|
return {"CANCELLED"}
|
|
# update status in search results for validator's clarity
|
|
search_results = search.get_search_results()
|
|
for result in search_results:
|
|
if result["id"] == self.asset_id:
|
|
result["verificationStatus"] = self.state
|
|
|
|
url = paths.BLENDERKIT_API + "/assets/" + str(self.asset_id) + "/"
|
|
upload_data = {"verificationStatus": self.state}
|
|
messages = {
|
|
"success": "Verification status changed",
|
|
"error": "Verification status change failed",
|
|
}
|
|
client_lib.nonblocking_request(url, "PATCH", {}, upload_data, messages)
|
|
|
|
if asset_bar_op.asset_bar_operator is not None:
|
|
asset_bar_op.asset_bar_operator.update_layout(context, None)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
# print(self.state)
|
|
if self.state == "deleted":
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
def handle_asset_upload(task: client_tasks.Task):
|
|
asset = eval(f"{task.data['export_data']['eval_path']}.blenderkit")
|
|
asset.upload_state = task.message
|
|
if task.status == "error":
|
|
asset.uploading = False
|
|
if task.result == {}:
|
|
return reports.add_report(
|
|
task.message, type="ERROR", details=task.message_detailed
|
|
)
|
|
|
|
# crazy shit to parse stupid Django incosistent error messages
|
|
if "detail" in task.result:
|
|
if type(task.result["detail"]) == dict:
|
|
for key in task.result["detail"]:
|
|
bk_logger.info("detail key " + str(key))
|
|
if type(task.result["detail"][key]) == list:
|
|
for item in task.result["detail"][key]:
|
|
asset.upload_state += f"\n- {key}: {item}"
|
|
else:
|
|
asset.upload_state += f"\n- {key}: {task.result['detail'][key]}"
|
|
return reports.add_report(
|
|
f"{task.message}: {task.result['detail']}",
|
|
type="ERROR",
|
|
details=task.message_detailed,
|
|
)
|
|
if type(task.result["detail"]) == list:
|
|
for item in task.result["detail"]:
|
|
asset.upload_state += f"\n- {item}"
|
|
return reports.add_report(
|
|
f"{task.message}: {task.result['detail']}",
|
|
type="ERROR",
|
|
details=task.message_detailed,
|
|
)
|
|
else:
|
|
asset.upload_state += f"\n {task.result}"
|
|
return reports.add_report(
|
|
f"{task.message}: {task.result}",
|
|
type="ERROR",
|
|
details=task.message_detailed,
|
|
)
|
|
|
|
if task.status == "finished":
|
|
asset.uploading = False
|
|
return reports.add_report("Upload successfull")
|
|
|
|
|
|
def handle_asset_metadata_upload(task: client_tasks.Task):
|
|
if task.status != "finished":
|
|
return
|
|
|
|
asset = eval(f"{task.data['export_data']['eval_path']}.blenderkit")
|
|
new_asset_base_id = task.result.get("assetBaseId", "")
|
|
if new_asset_base_id != "":
|
|
asset.asset_base_id = new_asset_base_id
|
|
bk_logger.info(f"Assigned new asset.asset_base_id: {new_asset_base_id}")
|
|
else:
|
|
asset.asset_base_id = task.data["export_data"]["assetBaseId"]
|
|
bk_logger.info(f"Assigned original asset.asset_base_id: {asset.asset_base_id}")
|
|
|
|
new_asset_id = task.result.get("id", "")
|
|
if new_asset_id != "":
|
|
asset.id = new_asset_id
|
|
bk_logger.info(f"Assigned new asset.id: {new_asset_id}")
|
|
else:
|
|
asset.id = task.data["export_data"]["id"]
|
|
bk_logger.info(f"Assigned original asset.id: {asset.id}")
|
|
|
|
return reports.add_report("Metadata upload successfull")
|
|
|
|
|
|
def patch_individual_parameter(asset_id="", param_name="", param_value="", api_key=""):
|
|
"""Changes individual parameter in the parameters dictionary of the assets.
|
|
|
|
Args:
|
|
asset_id (str): ID of the asset to update
|
|
param_name (str): Name of the parameter to update
|
|
param_value (str): New value for the parameter
|
|
api_key (str): BlenderKit API key
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
url = f"{paths.BLENDERKIT_API}/assets/{asset_id}/parameter/{param_name}/"
|
|
headers = utils.get_headers(api_key)
|
|
metadata_dict = {"value": param_value}
|
|
messages = {
|
|
"success": f"Successfully updated {param_name}",
|
|
"error": f"Failed to update {param_name}",
|
|
}
|
|
|
|
client_lib.nonblocking_request(
|
|
url=url,
|
|
method="PUT",
|
|
headers=headers,
|
|
json_data=metadata_dict,
|
|
messages=messages,
|
|
)
|
|
return True
|
|
|
|
|
|
def mark_for_thumbnail(
|
|
asset_id: str,
|
|
api_key: str,
|
|
# Common parameters
|
|
use_gpu: bool = None,
|
|
samples: int = None,
|
|
resolution: int = None,
|
|
denoising: bool = None,
|
|
background_lightness: float = None,
|
|
# Model-specific parameters
|
|
angle: str = None, # DEFAULT, FRONT, SIDE, TOP
|
|
snap_to: str = None, # GROUND, WALL, CEILING, FLOAT
|
|
# Material-specific parameters
|
|
thumbnail_type: str = None, # BALL, BALL_COMPLEX, FLUID, CLOTH, HAIR
|
|
scale: float = None,
|
|
background: bool = None,
|
|
adaptive_subdivision: bool = None,
|
|
) -> bool:
|
|
"""Mark an asset for thumbnail regeneration.
|
|
|
|
This function creates a JSON with thumbnail parameters and stores it in the
|
|
markThumbnailRender parameter of the asset. Only non-None parameters will be included.
|
|
|
|
Args:
|
|
asset_id (str): The ID of the asset to update
|
|
api_key (str): BlenderKit API key
|
|
use_gpu (bool, optional): Use GPU for rendering
|
|
samples (int, optional): Number of render samples
|
|
resolution (int, optional): Resolution of render
|
|
denoising (bool, optional): Use denoising
|
|
background_lightness (float, optional): Background lightness (0-1)
|
|
angle (str, optional): Camera angle for models (DEFAULT, FRONT, SIDE, TOP)
|
|
snap_to (str, optional): Object placement for models (GROUND, WALL, CEILING, FLOAT)
|
|
thumbnail_type (str, optional): Type of material preview (BALL, BALL_COMPLEX, FLUID, CLOTH, HAIR)
|
|
scale (float, optional): Scale of preview object for materials
|
|
background (bool, optional): Use background for transparent materials
|
|
adaptive_subdivision (bool, optional): Use adaptive subdivision for materials
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
# Build parameters dict with only non-None values
|
|
params = {}
|
|
|
|
# Common parameters
|
|
if use_gpu is not None:
|
|
params["thumbnail_use_gpu"] = use_gpu
|
|
if samples is not None:
|
|
params["thumbnail_samples"] = samples
|
|
if resolution is not None:
|
|
params["thumbnail_resolution"] = resolution
|
|
if denoising is not None:
|
|
params["thumbnail_denoising"] = denoising
|
|
if background_lightness is not None:
|
|
params["thumbnail_background_lightness"] = background_lightness
|
|
|
|
# Model-specific parameters
|
|
if angle is not None:
|
|
params["thumbnail_angle"] = angle
|
|
if snap_to is not None:
|
|
params["thumbnail_snap_to"] = snap_to
|
|
|
|
# Material-specific parameters
|
|
if thumbnail_type is not None:
|
|
params["thumbnail_type"] = thumbnail_type
|
|
if scale is not None:
|
|
params["thumbnail_scale"] = scale
|
|
if background is not None:
|
|
params["thumbnail_background"] = background
|
|
if adaptive_subdivision is not None:
|
|
params["thumbnail_adaptive_subdivision"] = adaptive_subdivision
|
|
|
|
try:
|
|
json_data = json.dumps(params)
|
|
return patch_individual_parameter(
|
|
asset_id, "markThumbnailRender", json_data, api_key
|
|
)
|
|
except Exception as e:
|
|
bk_logger.error(f"Failed to mark asset for thumbnail regeneration: {e}")
|
|
return False
|
|
|
|
|
|
def register_upload():
|
|
bpy.utils.register_class(UploadOperator)
|
|
bpy.utils.register_class(FastMetadata)
|
|
bpy.utils.register_class(AssetDebugPrint)
|
|
bpy.utils.register_class(AssetVerificationStatusChange)
|
|
|
|
|
|
def unregister_upload():
|
|
bpy.utils.unregister_class(UploadOperator)
|
|
bpy.utils.unregister_class(FastMetadata)
|
|
bpy.utils.unregister_class(AssetDebugPrint)
|
|
bpy.utils.unregister_class(AssetVerificationStatusChange)
|