2025-07-01
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.assets import AssetType
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from .. import reporting
|
||||
from ..toolbox import get_context
|
||||
from . import asset_browser as ab
|
||||
|
||||
|
||||
# https://blender.stackexchange.com/questions/249837/how-do-i-get-the-selected-assets-in-the-asset-browser-using-the-api
|
||||
class POLIIGON_OT_asset_browser_import(Operator):
|
||||
bl_idname = "poliigon.asset_browser_import"
|
||||
bl_label = _t("Import Selected Assets")
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
is_poliigon_lib = ab.is_poliigon_library(context)
|
||||
assets_selected = ab.get_num_selected_assets(context) > 0
|
||||
return is_poliigon_lib and assets_selected
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
num_selected = ab.get_num_selected_assets(context)
|
||||
if num_selected > 0:
|
||||
return _t("Import selected assets (default parameters)")
|
||||
else:
|
||||
return _t("No asset selected.\nPlease, select an asset")
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if not ab.is_poliigon_library(context):
|
||||
# As the operator should be shown for Poliigon Library, only
|
||||
# we shouldn't be here
|
||||
error_msg = ("POLIIGON_OT_asset_browser_import(): "
|
||||
"Poliigon library not selected!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_lib_not_sel", error_msg, "error")
|
||||
return {"CANCELLED"}
|
||||
|
||||
asset_files = ab.get_selected_assets(context)
|
||||
|
||||
for _asset_file in asset_files:
|
||||
asset_name = ab.get_asset_name_from_browser_asset(_asset_file)
|
||||
asset_data = ab.get_asset_data_from_browser_asset(_asset_file)
|
||||
if asset_data is None:
|
||||
error_msg = ("POLIIGON_OT_asset_browser_import(): "
|
||||
f"Asset {asset_name} not found!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_asset_not_found", error_msg, "error")
|
||||
cTB.logger_ab.error(error_msg)
|
||||
# TODO(Andreas): user notification
|
||||
continue
|
||||
|
||||
asset_type = asset_data.asset_type
|
||||
if asset_type == AssetType.HDRI:
|
||||
# TODO(Andreas): Do actual import
|
||||
pass
|
||||
elif asset_type == AssetType.MODEL:
|
||||
# TODO(Andreas): Do actual import
|
||||
pass
|
||||
elif asset_type == AssetType.TEXTURE:
|
||||
# TODO(Andreas): Do actual import
|
||||
pass
|
||||
else:
|
||||
error_msg = ("POLIIGON_OT_asset_browser_import():"
|
||||
f" Unexpected asset type: {asset_name} "
|
||||
f"{asset_type}")
|
||||
reporting.capture_message(
|
||||
"asset_browser_unexpected_type", error_msg, "error")
|
||||
cTB.logger_ab.error(error_msg)
|
||||
# TODO(Andreas): user notification
|
||||
continue
|
||||
|
||||
return {"FINISHED"}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..dialogs.dlg_quickmenu import show_quick_menu
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
from . import asset_browser as ab
|
||||
|
||||
|
||||
# https://blender.stackexchange.com/questions/249837/how-do-i-get-the-selected-assets-in-the-asset-browser-using-the-api
|
||||
class POLIIGON_OT_asset_browser_quick_menu(Operator):
|
||||
bl_idname = "poliigon.asset_browser_quick_menu"
|
||||
bl_label = _t("Show additional import options")
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
is_poliigon_lib = ab.is_poliigon_library(context)
|
||||
one_asset_selected = ab.get_num_selected_assets(context) == 1
|
||||
return is_poliigon_lib and one_asset_selected
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
num_selected = ab.get_num_selected_assets(context)
|
||||
if num_selected == 1:
|
||||
return _t("Show additional import options")
|
||||
elif num_selected == 0:
|
||||
return _t("No asset selected.\nPlease, select a single asset")
|
||||
else:
|
||||
return _t("Multiple assets selected.\nPlease, "
|
||||
"select a single asset, only")
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if not ab.is_poliigon_library(context):
|
||||
# As the operator should be shown for Poliigon Library
|
||||
# we shouldn't be here
|
||||
error_msg = ("POLIIGON_OT_asset_browser_quick_menu(): "
|
||||
"Poliigon library not selected!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_lib_not_sel", error_msg, "error")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# poll() makes sure, there's exactly one
|
||||
asset_file = ab.get_selected_assets(context)[0]
|
||||
|
||||
asset_name = ab.get_asset_name_from_browser_asset(asset_file)
|
||||
asset_data = ab.get_asset_data_from_browser_asset(asset_file)
|
||||
|
||||
if asset_data is None:
|
||||
error_msg = ("POLIIGON_OT_asset_browser_import(): "
|
||||
f"Asset {asset_name} not found!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_asset_not_found", error_msg, "error")
|
||||
self.report({"ERROR"}, f"Asset {asset_name} not found!")
|
||||
return {"CANCELLED"}
|
||||
|
||||
show_quick_menu(cTB, asset_data=asset_data)
|
||||
return {"FINISHED"}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
import bpy
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
from . import asset_browser as ab
|
||||
|
||||
|
||||
class POLIIGON_OT_asset_browser_reprocess(Operator):
|
||||
bl_idname = "poliigon.asset_browser_reprocess"
|
||||
bl_label = _t("Show additional import options")
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not ab.is_asset_browser(context):
|
||||
return False
|
||||
if not ab.is_poliigon_library(context):
|
||||
return False
|
||||
if not ab.is_only_poliigon_selected(context):
|
||||
return False
|
||||
if ab.get_num_selected_assets(context) == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
num_selected = ab.get_num_selected_assets(context)
|
||||
if num_selected == 0:
|
||||
return _t("No asset selected.\nPlease, select a single asset")
|
||||
else:
|
||||
return _t("Re-process selected assets")
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if not ab.is_poliigon_library(context):
|
||||
# As the operator should be shown for Poliigon Library
|
||||
# we shouldn't be here
|
||||
error_msg = ("POLIIGON_OT_asset_browser_reprocess(): "
|
||||
"Poliigon library not selected!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_reproc_not_sel", error_msg, "error")
|
||||
return {"CANCELLED"}
|
||||
|
||||
asset_files = ab.get_selected_assets(context)
|
||||
|
||||
for asset_file in asset_files:
|
||||
asset_name = ab.get_asset_name_from_browser_asset(asset_file)
|
||||
asset_data = ab.get_asset_data_from_browser_asset(asset_file)
|
||||
|
||||
if asset_data is None:
|
||||
error_msg = ("POLIIGON_OT_asset_browser_reprocess(): "
|
||||
f"Asset {asset_name} not found!")
|
||||
reporting.capture_message(
|
||||
"asset_browser_reproc_asset_missing", error_msg, "error")
|
||||
self.report({"ERROR"}, f"Asset {asset_name} not found!")
|
||||
continue
|
||||
|
||||
bpy.ops.poliigon.update_asset_browser(
|
||||
asset_id=asset_data.asset_id, force=True)
|
||||
|
||||
return {"FINISHED"}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# #### 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 #####
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
class POLIIGON_OT_cancel_asset_browser_sync(Operator):
|
||||
bl_idname = "poliigon.cancel_asset_browser"
|
||||
bl_label = _t("Cancel Asset Sync")
|
||||
bl_category = "Poliigon"
|
||||
bl_description = _t("Cancel synchronization of local assets with the "
|
||||
"Asset Browser")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
cTB.asset_browser_jobs_cancelled = True
|
||||
return {"FINISHED"}
|
||||
+963
@@ -0,0 +1,963 @@
|
||||
# #### 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 #####
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from queue import (
|
||||
Empty,
|
||||
Queue)
|
||||
import shutil
|
||||
import sys
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from uuid import uuid4
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
import bpy
|
||||
|
||||
from ..modules.poliigon_core.assets import (
|
||||
AssetData,
|
||||
AssetType)
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..material_import_utils import ASSET_TYPE_TO_IMPORTED_TYPE
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
from .asset_browser_sync_commands import (
|
||||
CMD_MARKER_START,
|
||||
CMD_MARKER_END,
|
||||
SyncCmd,
|
||||
SyncAssetBrowserCmd)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScriptContext():
|
||||
path_cat: Optional[str] = None # from command line args
|
||||
path_categories: Optional[str] = None # from command line args
|
||||
|
||||
poliigon_categories: Optional[Dict] = None
|
||||
|
||||
listener_running: bool = False
|
||||
thd_listener: Optional[Thread] = None
|
||||
|
||||
sender_running: bool = False
|
||||
thd_sender: Optional[Thread] = None
|
||||
|
||||
queue_cmd: Optional[Queue] = None
|
||||
queue_send: Optional[Queue] = None
|
||||
queue_ack: Optional[Queue] = None
|
||||
|
||||
main_running: bool = False
|
||||
|
||||
|
||||
class POLIIGON_OT_sync_client(Operator):
|
||||
bl_idname = "poliigon.asset_browser_sync_client"
|
||||
bl_label = _t("Sync Client")
|
||||
bl_category = "Poliigon"
|
||||
bl_description = _t("To be used in client Blender process to work on "
|
||||
"commands sent by host blender.")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
path_catalog: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
path_categories: StringProperty(options={"HIDDEN"}) # noqa: F821
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@staticmethod
|
||||
def _check_command(ctx: ScriptContext,
|
||||
buf: str
|
||||
) -> Tuple[Optional[SyncAssetBrowserCmd], str]:
|
||||
"""Returns a valid command, otherwise None.
|
||||
Upon detecting a corrupted command, CMD_ERROR gets sent.
|
||||
|
||||
Return value:
|
||||
Tuple with two entries:
|
||||
Tuple[0]: A valid command or None
|
||||
Tuple[1]: Remaining buf after either a valid command got detected or
|
||||
an broken command got removed
|
||||
"""
|
||||
|
||||
if CMD_MARKER_END not in buf:
|
||||
return None, buf
|
||||
|
||||
pos_delimiter = buf.find(CMD_MARKER_END, 1)
|
||||
cmd_json = buf[:pos_delimiter]
|
||||
buf = buf[pos_delimiter + len(CMD_MARKER_END):]
|
||||
|
||||
if CMD_MARKER_START in cmd_json:
|
||||
pos_marker_start = cmd_json.find(CMD_MARKER_START, 1)
|
||||
cmd_json = cmd_json[pos_marker_start + len(CMD_MARKER_START):]
|
||||
else:
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_ERROR))
|
||||
cmd_json = None
|
||||
return cmd_json, buf
|
||||
|
||||
@staticmethod
|
||||
def _thread_listener(ctx: ScriptContext) -> None:
|
||||
"""Listens to commands sent by host and checks their integrity.
|
||||
|
||||
In case of error requests a command to be re-send from host via
|
||||
CMD_ERROR.
|
||||
Valid commands are then sorted into two queues, one for received acks
|
||||
(forwarding them to unblock sender), one for job commands (forwarding
|
||||
them to main loop).
|
||||
"""
|
||||
|
||||
cTB.logger_ab.debug("thread_listener")
|
||||
ctx.listener_running = True
|
||||
buf = ""
|
||||
while ctx.listener_running:
|
||||
# Wait for messages from host, concatenating received lines
|
||||
# into buf
|
||||
try:
|
||||
buf += sys.stdin.readline()
|
||||
except KeyboardInterrupt:
|
||||
time.sleep(0.5)
|
||||
if ctx.listener_running:
|
||||
continue
|
||||
|
||||
if not ctx.listener_running:
|
||||
break
|
||||
|
||||
cmd_json, buf = POLIIGON_OT_sync_client._check_command(ctx, buf)
|
||||
if cmd_json is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
cmd_from_host = SyncAssetBrowserCmd.from_json(cmd_json)
|
||||
if cmd_from_host.code == SyncCmd.CMD_ERROR:
|
||||
# Forward ack to thread_sender
|
||||
ctx.queue_ack.put(cmd_from_host)
|
||||
else:
|
||||
# Forward job command to main loop
|
||||
ctx.queue_cmd.put(cmd_from_host)
|
||||
except Exception:
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_ERROR))
|
||||
cTB.logger_ab.exception(f"CMD ERROR {cmd_json}")
|
||||
|
||||
cTB.logger_ab.debug("thread_listener EXIT")
|
||||
ctx.thd_listener = None
|
||||
|
||||
@staticmethod
|
||||
def _start_listener(ctx: ScriptContext) -> None:
|
||||
"""Starts thread_listener()"""
|
||||
|
||||
ctx.thd_listener = Thread(
|
||||
target=POLIIGON_OT_sync_client._thread_listener,
|
||||
args=(ctx, ),
|
||||
daemon=True)
|
||||
ctx.thd_listener.start()
|
||||
|
||||
@staticmethod
|
||||
def _flush_queue_ack(ctx: ScriptContext) -> None:
|
||||
"""Removes all content from ack queue"""
|
||||
|
||||
while not ctx.queue_ack.empty():
|
||||
try:
|
||||
ctx.queue_ack.get_nowait()
|
||||
except ctx.queue_ack.Empty:
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def _shutdown_on_error(ctx: ScriptContext) -> None:
|
||||
"""Shuts the client down"""
|
||||
|
||||
# The client quits and host will pick up the "client loss" due to
|
||||
# timeouts
|
||||
ctx.sender_running = False
|
||||
ctx.listener_running = False
|
||||
ctx.main_running = False
|
||||
sys.stdin.close() # unblock listener
|
||||
|
||||
@staticmethod
|
||||
def _thread_sender(ctx: ScriptContext) -> None:
|
||||
"""Sends commands to host.
|
||||
|
||||
For commands expecting an acknowledge message the thread will then
|
||||
block until the ack is received (or possibly resend the command if
|
||||
CMD_ERROR is received).
|
||||
"""
|
||||
|
||||
cTB.logger_ab.debug("thread_sender")
|
||||
ctx.sender_running = True
|
||||
while ctx.sender_running:
|
||||
# Get rid of any unwanted acks from previous commands
|
||||
POLIIGON_OT_sync_client._flush_queue_ack(ctx)
|
||||
|
||||
# Wait for something to send
|
||||
try:
|
||||
cmd_send = ctx.queue_send.get(timeout=1.0)
|
||||
ctx.queue_send.task_done()
|
||||
except Empty:
|
||||
if ctx.sender_running:
|
||||
continue
|
||||
|
||||
if not ctx.sender_running:
|
||||
break
|
||||
|
||||
cTB.logger_ab.debug(f"Send: {cmd_send.code.name}")
|
||||
cmd_send.send_to_stdio()
|
||||
|
||||
# Depending on sent command code, we are already done
|
||||
if cmd_send.code in [SyncCmd.ASSET_OK, SyncCmd.ASSET_ERROR]:
|
||||
# ASSET_OK, ASSET_ERROR are fire and forget,
|
||||
# just proceed with next command
|
||||
continue
|
||||
elif cmd_send.code == SyncCmd.EXIT_ACK:
|
||||
# EXIT_ACK is fire and forget, we are done here
|
||||
ctx.sender_running = False
|
||||
break
|
||||
|
||||
# Wait for acknowledge message
|
||||
# TODO(Andreas): Currently this low retry count causes issues with
|
||||
# Patrick's library on a NAS and then likely exposes
|
||||
# a bug in timeout handling.
|
||||
retries = 3
|
||||
while retries > 0 and ctx.sender_running:
|
||||
try:
|
||||
cmd_ack = ctx.queue_ack.get(timeout=15.0)
|
||||
ctx.queue_ack.task_done()
|
||||
except Empty:
|
||||
cmd_ack = None
|
||||
|
||||
retries -= 1
|
||||
if cmd_ack is None:
|
||||
# queue timeout,
|
||||
# unless retries are exhausted continue to wait
|
||||
if retries == 0:
|
||||
# Unlikely we can gracefully recover
|
||||
POLIIGON_OT_sync_client._shutdown_on_error(ctx)
|
||||
break
|
||||
elif cmd_ack.code == SyncCmd.CMD_ERROR:
|
||||
# last sent command was not received well -> resend
|
||||
if retries > 0:
|
||||
cmd_send.send_to_stdio()
|
||||
else:
|
||||
# Unlikely we can gracefully recover
|
||||
POLIIGON_OT_sync_client._shutdown_on_error(ctx)
|
||||
elif cmd_ack.code == SyncCmd.CMD_DONE:
|
||||
# last sent command was ok, continue with next
|
||||
break
|
||||
|
||||
cTB.logger_ab.debug("thread_sender EXIT")
|
||||
ctx.thd_sender = None
|
||||
|
||||
@staticmethod
|
||||
def _start_sender(ctx: ScriptContext) -> None:
|
||||
"""Starts thread_sender()"""
|
||||
|
||||
ctx.thd_sender = Thread(
|
||||
target=POLIIGON_OT_sync_client._thread_sender,
|
||||
args=(ctx, ),
|
||||
daemon=True)
|
||||
ctx.thd_sender.start()
|
||||
|
||||
@staticmethod
|
||||
def _startup(ctx: ScriptContext) -> None:
|
||||
cTB.logger_ab.debug("waiting for asset data...")
|
||||
bpy.ops.poliigon.get_local_asset_sync(
|
||||
await_startup_poliigon=False,
|
||||
await_startup_my_assets=True,
|
||||
get_poliigon=False,
|
||||
get_my_assets=False,
|
||||
abort_ongoing_jobs=False)
|
||||
cTB.logger_ab.debug("...done.")
|
||||
|
||||
if not POLIIGON_OT_sync_client._read_poliigon_categories(ctx):
|
||||
return False
|
||||
|
||||
ctx.queue_cmd = Queue()
|
||||
ctx.queue_send = Queue()
|
||||
ctx.queue_ack = Queue()
|
||||
|
||||
POLIIGON_OT_sync_client._start_listener(ctx)
|
||||
POLIIGON_OT_sync_client._start_sender(ctx)
|
||||
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.HELLO))
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _reset_blend():
|
||||
"""Prepares a fresh blend file for stuff to be imported into."""
|
||||
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
# To be safe deselect all
|
||||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
@staticmethod
|
||||
def _save_blend(path: str) -> bool:
|
||||
"""Saves the current blend file."""
|
||||
|
||||
# Remove previous file
|
||||
# (host will re-process assets only, if force parameter was set)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
path_norm = os.path.normpath(path)
|
||||
result = bpy.ops.wm.save_mainfile(filepath=path_norm,
|
||||
check_existing=False,
|
||||
exit=False)
|
||||
return result == {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def _get_unique_uuid(catalog_dict: Dict) -> str:
|
||||
"""Returns a new, random UUID, which does not already exist
|
||||
in catalog.
|
||||
"""
|
||||
|
||||
uuid_is_unique = False
|
||||
while not uuid_is_unique:
|
||||
uuid_result = str(uuid4())
|
||||
uuid_is_unique = True
|
||||
for uuid_existing, _, _ in catalog_dict.values():
|
||||
if uuid_result == uuid_existing:
|
||||
uuid_is_unique = False
|
||||
break
|
||||
return uuid_result
|
||||
|
||||
# Based on code from:
|
||||
# https://blender.stackexchange.com/questions/249316/python-set-asset-library-tags-and-catalogs
|
||||
@staticmethod
|
||||
def _get_catalog_dict(ctx: ScriptContext) -> Dict:
|
||||
"""Reads blender's catalogue and returns a dictionary with its content.
|
||||
|
||||
Return value:
|
||||
Dict: {catalog tree path: (uuid, catalog tree path, catalog name)}
|
||||
"""
|
||||
|
||||
if not os.path.exists(ctx.path_cat):
|
||||
return {}
|
||||
catalogs = {}
|
||||
with open(ctx.path_cat, "r") as file_catalogs:
|
||||
for line in file_catalogs.readlines():
|
||||
if line.startswith(("#", "VERSION", "\n")):
|
||||
continue
|
||||
# Each line contains:
|
||||
# 'uuid:catalog_tree:catalog_name' + eol ('\n')
|
||||
uuid, tree_path, name = line.split(":")
|
||||
name = name.split("\n")[0]
|
||||
catalogs[tree_path] = (uuid, tree_path, name)
|
||||
return catalogs
|
||||
|
||||
@staticmethod
|
||||
def _catalog_file_header(version: int = 1):
|
||||
"""Returns the standard header of a catalog file."""
|
||||
|
||||
header = (
|
||||
"# This is an Asset Catalog Definition file for Blender.\n"
|
||||
"#\n"
|
||||
"# Empty lines and lines starting with `#` will be ignored.\n"
|
||||
"# The first non-ignored line should be the version indicator.\n"
|
||||
'# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"\n'
|
||||
"\n"
|
||||
f"VERSION {version}\n"
|
||||
"\n")
|
||||
return header
|
||||
|
||||
@staticmethod
|
||||
def _write_catalog_file(ctx: ScriptContext, catalog_dict: Dict) -> bool:
|
||||
"""Writes a catalog dict into a new catalog file,
|
||||
replacing the old file upon success.
|
||||
"""
|
||||
|
||||
path_cat_temp = ctx.path_cat + ".TEMP"
|
||||
path_cat_bak = ctx.path_cat + ".BAK"
|
||||
try:
|
||||
# Write into temporary file
|
||||
with open(path_cat_temp, "w") as file_catalogs:
|
||||
header = POLIIGON_OT_sync_client._catalog_file_header()
|
||||
file_catalogs.write(header)
|
||||
for _uuid, tree_path, name in catalog_dict.values():
|
||||
file_catalogs.write(f"{_uuid}:{tree_path}:{name}\n")
|
||||
|
||||
# Replace existing catalog file (if any) with above temporary file
|
||||
if os.path.exists(ctx.path_cat):
|
||||
shutil.move(ctx.path_cat, path_cat_bak)
|
||||
shutil.move(path_cat_temp, ctx.path_cat)
|
||||
if os.path.exists(path_cat_bak):
|
||||
os.remove(path_cat_bak)
|
||||
except IsADirectoryError:
|
||||
# Should not occur, it's our files
|
||||
cTB.logger_ab.exception("IsADirectoryError")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
# Should not occur, it's being tested above
|
||||
cTB.logger_ab.exception("FileNotFoundError")
|
||||
return False
|
||||
except OSError:
|
||||
# Faied to create file
|
||||
cTB.logger_ab.exception("OSError")
|
||||
return False
|
||||
except Exception:
|
||||
cTB.logger_ab.exception("Unexpected exception!")
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _read_poliigon_categories(ctx: ScriptContext) -> bool:
|
||||
"""Reads all Poliigon categories into a dict in context."""
|
||||
|
||||
if not os.path.exists(ctx.path_categories):
|
||||
cTB.logger_ab.debug("Poliigon categories file missing")
|
||||
ctx.poliigon_categories = {"HDRIs": [],
|
||||
"Models": [],
|
||||
"Textures": []
|
||||
}
|
||||
return False
|
||||
|
||||
with open(ctx.path_categories, "r") as file_categories:
|
||||
try:
|
||||
ctx.poliigon_categories = json.load(file_categories)
|
||||
except json.JSONDecodeError:
|
||||
cTB.logger_ab.debug("Poliigon's category file is corrupt!")
|
||||
return False
|
||||
|
||||
ctx.poliigon_categories = ctx.poliigon_categories["poliigon"]
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_unique_category_list(
|
||||
ctx: ScriptContext, asset_data: AssetData) -> List[str]:
|
||||
"""Returns a list of categories matching the first (alphabetically)
|
||||
matching branch in Poliigon's category tree."""
|
||||
|
||||
asset_type = asset_data.asset_type
|
||||
asset_name = asset_data.asset_name
|
||||
|
||||
asset_type_cat = ASSET_TYPE_TO_IMPORTED_TYPE[asset_type]
|
||||
if asset_type_cat not in ctx.poliigon_categories:
|
||||
cTB.logger_ab.debug("!!! Asset type not found "
|
||||
f"{asset_name} {asset_type}")
|
||||
cTB.logger_ab.debug(" Category types "
|
||||
f"{list(ctx.poliigon_categories.keys())}")
|
||||
return [asset_type.name]
|
||||
|
||||
# Have copy, as we are removing some categorie during the process
|
||||
asset_categories = asset_data.categories.copy()
|
||||
|
||||
if "free" in asset_categories:
|
||||
asset_categories.remove("free")
|
||||
if asset_type_cat in asset_categories:
|
||||
# It gets prepended anyway in next step
|
||||
asset_categories.remove(asset_type_cat)
|
||||
|
||||
category_list = [asset_type_cat]
|
||||
cat_slug = ""
|
||||
for cat in asset_categories:
|
||||
cat = cat.title()
|
||||
cat_slug += "/" + cat
|
||||
if cat_slug not in ctx.poliigon_categories[asset_type_cat]:
|
||||
break
|
||||
category_list.append(cat)
|
||||
|
||||
return category_list
|
||||
|
||||
@staticmethod
|
||||
def _add_catalog(
|
||||
ctx: ScriptContext, asset_data: AssetData, entity: Any) -> bool:
|
||||
"""Assigns a catalog to an entity (object, collection, material,
|
||||
world,...).
|
||||
|
||||
If needed, the catalog file will be extended with additional catalogs
|
||||
based on the categories of the asset.
|
||||
"""
|
||||
|
||||
catalog_dict = POLIIGON_OT_sync_client._get_catalog_dict(ctx)
|
||||
asset_categories = POLIIGON_OT_sync_client._get_unique_category_list(
|
||||
ctx, asset_data)
|
||||
|
||||
# After this loop uuid_result contains the UUID of the leaf catalog
|
||||
for idx_cat, category in enumerate(asset_categories):
|
||||
category_path = "/".join(asset_categories[:idx_cat + 1])
|
||||
if category_path not in catalog_dict:
|
||||
uuid_result = POLIIGON_OT_sync_client._get_unique_uuid(
|
||||
catalog_dict)
|
||||
catalog_dict[category_path] = (uuid_result,
|
||||
category_path,
|
||||
category)
|
||||
else:
|
||||
uuid_result, _, _ = catalog_dict[category_path]
|
||||
|
||||
if not POLIIGON_OT_sync_client._write_catalog_file(
|
||||
ctx, catalog_dict):
|
||||
cTB.logger_ab.debug("add_catalog(): Failed to write catalog file")
|
||||
return False
|
||||
|
||||
# Finally assign the determined UUID to the entity
|
||||
entity.asset_data.catalog_id = uuid_result
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _assign_asset_tags(
|
||||
asset_data: AssetData, entity: Any, params: Dict) -> None:
|
||||
"""Assigns tags to an entity (object, collection, material, world,...).
|
||||
|
||||
NOTE: This function requires entity.asset_mark() to be called
|
||||
beforehand.
|
||||
|
||||
Args:
|
||||
asset_data: AssetData
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
asset_name = asset_data.asset_name
|
||||
asset_display_name = asset_data.display_name
|
||||
asset_type = asset_data.asset_type
|
||||
|
||||
entity.asset_data.tags.new(asset_display_name)
|
||||
entity.asset_data.tags.new(asset_name) # unique name
|
||||
entity.asset_data.tags.new("Poliigon")
|
||||
for category in asset_data.categories:
|
||||
# TODO(Andreas): maybe we want to filter free?
|
||||
entity.asset_data.tags.new(category.title())
|
||||
|
||||
if asset_type == AssetType.HDRI:
|
||||
entity.asset_data.tags.new(params["size"])
|
||||
entity.asset_data.tags.new(params["size_bg"])
|
||||
elif asset_type == AssetType.MODEL:
|
||||
entity.asset_data.tags.new(params["size"])
|
||||
entity.asset_data.tags.new(params["lod"])
|
||||
elif asset_type == AssetType.TEXTURE:
|
||||
entity.asset_data.tags.new(params["size"])
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported asset type: {asset_type}")
|
||||
|
||||
@staticmethod
|
||||
def _assign_asset_preview(
|
||||
asset_data: AssetData, entity: Any, params: Dict) -> None:
|
||||
"""Assigns a preview image to an entity (object, collection, material,
|
||||
world,...).
|
||||
|
||||
NOTE: This function requires entity.asset_mark() to be called
|
||||
beforehand.
|
||||
|
||||
Args:
|
||||
asset_data: AssetData
|
||||
entity: Blender's object, collection, material, ...
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
path_thumb = params["thumb"]
|
||||
is_path = path_thumb is not None and len(path_thumb) > 2
|
||||
if is_path and os.path.exists(path_thumb):
|
||||
# From: https://blender.stackexchange.com/questions/6101/poll-failed-context-incorrect-example-bpy-ops-view3d-background-image-add
|
||||
# and: https://blender.stackexchange.com/questions/245397/batch-assign-pre-existing-image-files-as-asset-previews
|
||||
|
||||
# equal to: if bpy.app.version >= (3, 2, 0)
|
||||
if hasattr(bpy.context, "temp_override"):
|
||||
with bpy.context.temp_override(id=entity):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=path_thumb)
|
||||
else:
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
{"id": entity}, filepath=path_thumb)
|
||||
else:
|
||||
# TODO(Andreas): Not working as expected
|
||||
# Maybe https://developer.blender.org/T93893 ?
|
||||
entity.asset_generate_preview()
|
||||
|
||||
@staticmethod
|
||||
def _assign_asset_meta_data(ctx: ScriptContext,
|
||||
asset_data: AssetData,
|
||||
entity: Any,
|
||||
params: Dict) -> bool:
|
||||
"""Assigns all meta data (e.g. author, tags, preview, catalog...) to an
|
||||
entity (object, collection, material, world,...).
|
||||
|
||||
Args:
|
||||
ctx: ScriptContext instance created upon script start
|
||||
asset_data: AssetData
|
||||
entity: Blender's object, collection, material, ...
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
if hasattr(entity, "type"):
|
||||
type_label = f", type: {entity.type}"
|
||||
elif isinstance(entity, bpy.types.Material):
|
||||
type_label = ", type: Material"
|
||||
else:
|
||||
type_label = ", type: UNKNOWN"
|
||||
cTB.logger_ab.debug(f"Marking {entity.name} {type_label}")
|
||||
|
||||
entity.asset_mark()
|
||||
|
||||
entity.asset_data.author = "Poliigon"
|
||||
entity.asset_data.description = asset_data.display_name
|
||||
|
||||
try:
|
||||
POLIIGON_OT_sync_client._assign_asset_tags(
|
||||
asset_data, entity, params)
|
||||
except NotImplementedError:
|
||||
cTB.logger_ab.exception("Unsupported Asset Type")
|
||||
return False
|
||||
POLIIGON_OT_sync_client._assign_asset_preview(
|
||||
asset_data, entity, params)
|
||||
if not POLIIGON_OT_sync_client._add_catalog(ctx, asset_data, entity):
|
||||
cTB.logger_ab.debug(
|
||||
"assign_asset_meta_data(): Failed to add catalog")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _process_hdri(
|
||||
ctx: ScriptContext, asset_data: AssetData, params: Dict) -> bool:
|
||||
"""Processes an HDRI asset.
|
||||
|
||||
Args:
|
||||
ctx: ScriptContext instance created upon script start
|
||||
asset_data: An asset data dict passed down from P4B host.
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
if "size" not in params or "thumb" not in params:
|
||||
cTB.logger_ab.debug(
|
||||
"Missing required parameter (size and/or thumb) to process "
|
||||
"HDRI")
|
||||
return False
|
||||
|
||||
asset_id = asset_data.asset_id
|
||||
asset_name = asset_data.asset_name
|
||||
size = params["size"]
|
||||
size_bg = params["size_bg"]
|
||||
|
||||
cTB.logger_ab.debug(f"process_hdri {asset_name} {size}")
|
||||
try:
|
||||
result = bpy.ops.poliigon.poliigon_hdri(
|
||||
asset_id=asset_id,
|
||||
size=size,
|
||||
size_bg=size_bg)
|
||||
except Exception:
|
||||
cTB.logger_ab.exception("HDRI ERROR")
|
||||
return False
|
||||
|
||||
if result != {"FINISHED"}:
|
||||
return False
|
||||
|
||||
# Rename world,
|
||||
# otherwise the asset would appear as "World" in the Asset Browser.
|
||||
world = bpy.context.scene.world
|
||||
world.name = asset_name
|
||||
|
||||
if not POLIIGON_OT_sync_client._assign_asset_meta_data(
|
||||
ctx, asset_data, world, params):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _process_model(
|
||||
ctx: ScriptContext, asset_data: AssetData, params: Dict) -> bool:
|
||||
"""Processes a Model asset.
|
||||
|
||||
Args:
|
||||
ctx: ScriptContext instance created upon script start
|
||||
asset_data: AssetData
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
has_size = "size" in params
|
||||
has_lod = "lod" in params
|
||||
has_thumb = "thumb" in params
|
||||
if not has_size or not has_lod or not has_thumb:
|
||||
cTB.logger_ab.debug(
|
||||
"Missing required parameter (size, lod and/or thumb) to "
|
||||
"process Model")
|
||||
return False
|
||||
|
||||
asset_id = asset_data.asset_id
|
||||
asset_name = asset_data.asset_name
|
||||
|
||||
size = params["size"]
|
||||
lod = params["lod"]
|
||||
|
||||
cTB.logger_ab.debug(f"process_model {asset_name} {size} {lod}")
|
||||
|
||||
try:
|
||||
result = bpy.ops.poliigon.poliigon_model(
|
||||
asset_id=asset_id,
|
||||
size=size,
|
||||
lod=lod,
|
||||
do_use_collection=True,
|
||||
do_link_blend=True,
|
||||
do_reuse_materials=False)
|
||||
except Exception:
|
||||
cTB.logger_ab.exception("MODEL ERROR")
|
||||
return False
|
||||
|
||||
if result != {"FINISHED"}:
|
||||
cTB.logger_ab.error("LOAD FAILURE")
|
||||
return False
|
||||
|
||||
# Mark the object instancing our collection
|
||||
found = False
|
||||
error = False
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != "EMPTY":
|
||||
continue
|
||||
if obj.parent is not None:
|
||||
continue
|
||||
if not obj.name.startswith(asset_name):
|
||||
continue
|
||||
if obj.instance_collection is None:
|
||||
continue
|
||||
|
||||
if POLIIGON_OT_sync_client._assign_asset_meta_data(
|
||||
ctx, asset_data, obj, params):
|
||||
found = True
|
||||
else:
|
||||
error = True
|
||||
break
|
||||
return found and not error
|
||||
|
||||
@staticmethod
|
||||
def _process_texture(
|
||||
ctx: ScriptContext, asset_data: AssetData, params: Dict) -> bool:
|
||||
"""Processes a Texture asset (including backplates and backdrops).
|
||||
|
||||
Args:
|
||||
ctx: ScriptContext instance created upon script start
|
||||
asset_data: AssetData
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
if "size" not in params or "thumb" not in params:
|
||||
cTB.logger_ab.debug(
|
||||
"Missing required parameter (size and/or thumb) to process "
|
||||
"Texture")
|
||||
return False
|
||||
|
||||
asset_id = asset_data.asset_id
|
||||
asset_name = asset_data.asset_name
|
||||
size = params["size"]
|
||||
|
||||
cTB.logger_ab.debug(f"process_texture {asset_name} {size}")
|
||||
|
||||
try:
|
||||
result = bpy.ops.poliigon.poliigon_material(
|
||||
asset_id=asset_id, size=size)
|
||||
except Exception:
|
||||
cTB.logger_ab.exception("MATERIAL ERROR")
|
||||
return False
|
||||
|
||||
if result != {"FINISHED"}:
|
||||
cTB.logger_ab.debug(
|
||||
f"Operator poliigon_material returned: {result}")
|
||||
return False
|
||||
|
||||
found = False
|
||||
error = False
|
||||
for mat in bpy.data.materials:
|
||||
if not mat.name.startswith(asset_name):
|
||||
continue
|
||||
|
||||
if POLIIGON_OT_sync_client._assign_asset_meta_data(
|
||||
ctx, asset_data, mat, params):
|
||||
found = True
|
||||
else:
|
||||
error = True
|
||||
cTB.logger_ab.debug(
|
||||
f"Failed to assign meta data to material: {mat.name}")
|
||||
break
|
||||
|
||||
if not found:
|
||||
cTB.logger_ab.debug("Found no entity to mark")
|
||||
|
||||
return found and not error
|
||||
|
||||
@staticmethod
|
||||
def _process_asset(
|
||||
ctx: ScriptContext, asset_data: AssetData, params: Dict) -> bool:
|
||||
"""Creates and saves an Asset Browser-marked asset to a new blend file.
|
||||
|
||||
Args:
|
||||
ctx: ScriptContext instance created upon script start
|
||||
asset_data: AssetData
|
||||
params: Populated by host in function
|
||||
asset_browser.py:get_asset_job_parameters()
|
||||
"""
|
||||
|
||||
if "path_result" not in params:
|
||||
cTB.logger_ab.debug("process_asset(): Lacking result path!")
|
||||
return False
|
||||
path_result = params["path_result"]
|
||||
|
||||
POLIIGON_OT_sync_client._reset_blend()
|
||||
|
||||
asset_name = asset_data.asset_name
|
||||
asset_type = asset_data.asset_type
|
||||
|
||||
cTB.logger_ab.debug(f"process_asset() {asset_name}")
|
||||
for _param, value in params.items():
|
||||
cTB.logger_ab.debug(f" {_param} {value}")
|
||||
|
||||
if asset_type == AssetType.HDRI:
|
||||
result = POLIIGON_OT_sync_client._process_hdri(
|
||||
ctx, asset_data, params)
|
||||
elif asset_type == AssetType.MODEL:
|
||||
result = POLIIGON_OT_sync_client._process_model(
|
||||
ctx, asset_data, params)
|
||||
elif asset_type == AssetType.TEXTURE:
|
||||
result = POLIIGON_OT_sync_client._process_texture(
|
||||
ctx, asset_data, params)
|
||||
else:
|
||||
cTB.logger_ab.debug("process_asset(): Unknown asset type")
|
||||
return False
|
||||
|
||||
if result:
|
||||
result = POLIIGON_OT_sync_client._save_blend(path_result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _cmd_asset(ctx: ScriptContext, cmd: SyncAssetBrowserCmd) -> None:
|
||||
"""Handle an ASSET command"""
|
||||
|
||||
asset_id = cmd.data["asset_id"]
|
||||
asset_data = cTB._asset_index.get_asset(asset_id)
|
||||
if asset_data is None:
|
||||
bpy.ops.poliigon.get_local_asset_sync(
|
||||
await_startup_poliigon=False,
|
||||
await_startup_my_assets=False,
|
||||
get_poliigon=False,
|
||||
get_my_assets=True,
|
||||
asset_id=asset_id,
|
||||
abort_ongoing_jobs=False)
|
||||
asset_data = cTB._asset_index.get_asset(asset_id)
|
||||
|
||||
if asset_data is not None:
|
||||
result = POLIIGON_OT_sync_client._process_asset(
|
||||
ctx, asset_data, cmd.params)
|
||||
else:
|
||||
cTB.logger_ab.error(f"process_asset(): No asset data for {asset_id}.")
|
||||
result = False
|
||||
|
||||
if result:
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.ASSET_OK,
|
||||
data=cmd.data))
|
||||
else:
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.ASSET_ERROR,
|
||||
data=cmd.data))
|
||||
cTB.logger_ab.debug(f"cmd_asset exit {asset_data.asset_name}")
|
||||
|
||||
@staticmethod
|
||||
def _cmd_hello_ok(ctx: ScriptContext, cmd: SyncAssetBrowserCmd) -> None:
|
||||
"""Handle a HELLO_OK command"""
|
||||
|
||||
cTB.logger_ab.debug("cmd_hello_ok")
|
||||
ctx.queue_ack.put(SyncAssetBrowserCmd(code=SyncCmd.CMD_DONE))
|
||||
|
||||
@staticmethod
|
||||
def _cmd_still_there(ctx: ScriptContext, cmd: SyncAssetBrowserCmd) -> None:
|
||||
"""Handle a STILL_THERE command"""
|
||||
|
||||
cTB.logger_ab.debug("cmd_still_there")
|
||||
bpy.ops.poliigon.get_local_asset_sync(
|
||||
await_startup_poliigon=False,
|
||||
await_startup_my_assets=True,
|
||||
get_poliigon=False,
|
||||
get_my_assets=False,
|
||||
abort_ongoing_jobs=False)
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.HELLO))
|
||||
|
||||
@staticmethod
|
||||
def _cmd_exit(ctx: ScriptContext, cmd: SyncAssetBrowserCmd) -> None:
|
||||
"""Handle an EXIT command"""
|
||||
|
||||
cTB.logger_ab.debug("cmd_exit")
|
||||
# Notify host, we are going to exit
|
||||
ctx.queue_send.put(SyncAssetBrowserCmd(code=SyncCmd.EXIT_ACK))
|
||||
# Tear down everything
|
||||
ctx.listener_running = False
|
||||
if ctx.thd_listener is not None:
|
||||
ctx.thd_listener.join()
|
||||
if ctx.thd_sender is not None:
|
||||
ctx.thd_sender.join()
|
||||
ctx.main_running = False
|
||||
|
||||
def _init_settings(self) -> None:
|
||||
"""Changes settings to what is needed by sync client
|
||||
(backing up the original settings).
|
||||
"""
|
||||
|
||||
self.settings_backup = {}
|
||||
for _key in ["download_prefer_blend",
|
||||
"download_link_blend"]:
|
||||
self.settings_backup[_key] = cTB.settings[_key]
|
||||
|
||||
cTB.settings["download_prefer_blend"] = 1
|
||||
cTB.settings["download_link_blend"] = 0
|
||||
|
||||
def _restore_settings(self) -> None:
|
||||
"""Restores settings from backup."""
|
||||
|
||||
for _key in ["download_prefer_blend",
|
||||
"download_link_blend"]:
|
||||
cTB.settings[_key] = self.settings_backup[_key]
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
has_catalog = self.path_catalog is not None
|
||||
has_categories = self.path_categories is not None
|
||||
if not has_catalog or not has_categories:
|
||||
return {"CANCELLED"}
|
||||
|
||||
ctx = ScriptContext(
|
||||
path_cat=self.path_catalog,
|
||||
path_categories=self.path_categories)
|
||||
|
||||
if not self._startup(ctx):
|
||||
return {"CANCELLED"}
|
||||
|
||||
ctx.main_running = True
|
||||
while ctx.main_running:
|
||||
try:
|
||||
cmd_recv = ctx.queue_cmd.get(timeout=1.0)
|
||||
ctx.queue_cmd.task_done()
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
if cmd_recv is None:
|
||||
continue
|
||||
|
||||
if cmd_recv.code == SyncCmd.EXIT:
|
||||
self._cmd_exit(ctx, cmd_recv)
|
||||
elif cmd_recv.code == SyncCmd.ASSET:
|
||||
self._init_settings()
|
||||
self._cmd_asset(ctx, cmd_recv)
|
||||
self._restore_settings()
|
||||
elif cmd_recv.code == SyncCmd.STILL_THERE:
|
||||
self._cmd_still_there(ctx, cmd_recv)
|
||||
elif cmd_recv.code == SyncCmd.HELLO_OK:
|
||||
self._cmd_hello_ok(ctx, cmd_recv)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,90 @@
|
||||
# #### 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 #####
|
||||
|
||||
from threading import Thread
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty
|
||||
)
|
||||
import bpy
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from ..constants import ASSET_ID_ALL
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
from . import asset_browser as ab
|
||||
|
||||
|
||||
class POLIIGON_OT_update_asset_browser(Operator):
|
||||
bl_idname = "poliigon.update_asset_browser"
|
||||
bl_label = _t("Sync Local Assets")
|
||||
bl_category = "Poliigon"
|
||||
bl_description = _t("Synchronize local assets with Asset Browser")
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
asset_id: IntProperty(options={"HIDDEN"}, default=ASSET_ID_ALL) # noqa: F821
|
||||
force: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
|
||||
|
||||
@staticmethod
|
||||
def init_context(addon_version: str) -> None:
|
||||
"""Called from operators.py to init global addon context."""
|
||||
|
||||
global cTB
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
@reporting.handle_operator(silent=True)
|
||||
def execute(self, context):
|
||||
if bpy.app.version < (3, 0):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Asset browser not available in this blender version")
|
||||
return {"CANCELLED"}
|
||||
|
||||
bpy.ops.poliigon.get_local_asset_sync(
|
||||
await_startup_poliigon=False,
|
||||
await_startup_my_assets=False,
|
||||
get_poliigon=False,
|
||||
get_my_assets=True,
|
||||
asset_id=self.asset_id)
|
||||
|
||||
if self.asset_id != ASSET_ID_ALL:
|
||||
asset_ids = cTB._asset_index.get_asset_id_list(
|
||||
purchased=True, local=True)
|
||||
if self.asset_id not in asset_ids:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Asset ID {self.asset_id} not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if ab.create_poliigon_library() is None:
|
||||
cTB.logger_ab.debug("HOST: No Poliigon library in Asset Browser!")
|
||||
error_msg = "No Poliigon library in Asset."
|
||||
reporting.capture_message(
|
||||
"asset_browser_no_polii_lib", error_msg, "error")
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
thd_init_sync = Thread(
|
||||
target=ab.thread_initiate_asset_synchronization,
|
||||
args=(self.asset_id, self.force, ))
|
||||
thd_init_sync.start()
|
||||
cTB.threads.append(thd_init_sync)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,47 @@
|
||||
# #### 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 bpy
|
||||
|
||||
from .asset_browser_operator_import import POLIIGON_OT_asset_browser_import
|
||||
from .asset_browser_operator_quick_menu import POLIIGON_OT_asset_browser_quick_menu
|
||||
from .asset_browser_operator_reprocess import POLIIGON_OT_asset_browser_reprocess
|
||||
from .asset_browser_operator_sync_cancel import POLIIGON_OT_cancel_asset_browser_sync
|
||||
from .asset_browser_operator_sync_client import POLIIGON_OT_sync_client
|
||||
from .asset_browser_operator_update import POLIIGON_OT_update_asset_browser
|
||||
|
||||
|
||||
classes = (
|
||||
POLIIGON_OT_update_asset_browser,
|
||||
POLIIGON_OT_cancel_asset_browser_sync,
|
||||
POLIIGON_OT_asset_browser_import,
|
||||
POLIIGON_OT_asset_browser_quick_menu,
|
||||
POLIIGON_OT_asset_browser_reprocess,
|
||||
POLIIGON_OT_sync_client
|
||||
)
|
||||
|
||||
|
||||
def register(addon_version: str):
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
cls.init_context(addon_version)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,129 @@
|
||||
# #### 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 #####
|
||||
|
||||
"""Standalone blender startup script used to generate asset blend files.
|
||||
|
||||
General Asset Browser links:
|
||||
Asset Catalogs: https://wiki.blender.org/wiki/Source/Architecture/Asset_System/Catalogs
|
||||
Asset Operators: https://docs.blender.org/api/current/bpy.ops.asset.html
|
||||
AssetMetaData: https://docs.blender.org/api/current/bpy.types.AssetMetaData.html#bpy.types.AssetMetaData
|
||||
Catalogs and save: https://blender.stackexchange.com/questions/284833/get-asset-browser-catalogs-in-case-of-unsaved-changes
|
||||
|
||||
Operator overriding:
|
||||
https://blender.stackexchange.com/questions/248274/a-comprehensive-list-of-operator-overrides
|
||||
https://blender.stackexchange.com/questions/129989/override-context-for-operator-called-from-panel
|
||||
https://blender.stackexchange.com/questions/182713/how-to-use-context-override-on-the-disable-and-keep-transform-operator
|
||||
https://blender.stackexchange.com/questions/273474/how-to-override-context-to-launch-ops-commands-in-text-editor-3-2
|
||||
https://blender.stackexchange.com/questions/875/proper-bpy-ops-context-setup-in-a-plugin
|
||||
|
||||
Asset browser related, not much use in here:
|
||||
https://blender.stackexchange.com/questions/262284/how-do-i-access-the-list-of-selected-assets-from-an-event-in-python
|
||||
https://blender.stackexchange.com/questions/261213/get-the-source-path-of-the-assets-in-asset-browser-using-python
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
from typing import Tuple
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from modules.poliigon_core.multilingual import _t # noqa: E402
|
||||
from constants import ADDON_NAME # noqa: E402
|
||||
|
||||
|
||||
DEBUG_CLIENT = False
|
||||
|
||||
|
||||
def print_debug(*args, file=sys.stdout) -> None:
|
||||
"""Use for printing in client script"""
|
||||
|
||||
if not DEBUG_CLIENT:
|
||||
return
|
||||
print(" C:", *args, file=file)
|
||||
|
||||
|
||||
def command_line_args() -> Tuple[str, str]:
|
||||
"""Parses command line args."""
|
||||
|
||||
path_catalog = None
|
||||
path_categories = None
|
||||
|
||||
# Skip Blender's own command line args
|
||||
argv = sys.argv
|
||||
try:
|
||||
idx_arg = argv.index("--") + 1
|
||||
except ValueError:
|
||||
idx_arg = None
|
||||
if idx_arg is None or idx_arg >= len(argv):
|
||||
return None, None
|
||||
|
||||
argv = argv[idx_arg:]
|
||||
|
||||
try:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-pcf", "--poliigon_cat_file",
|
||||
help=_t("Path to catalog file"),
|
||||
required=True)
|
||||
parser.add_argument("-pc", "--poliigon_categories",
|
||||
help=_t("Path to file with Poliigon categories"),
|
||||
required=True)
|
||||
args = parser.parse_args(argv)
|
||||
except Exception as e:
|
||||
print_debug(e)
|
||||
|
||||
if args.poliigon_cat_file:
|
||||
path_catalog = args.poliigon_cat_file
|
||||
else:
|
||||
print_debug(
|
||||
"Lacking path to Blender cat file in commandline arguments!")
|
||||
return None, None
|
||||
|
||||
if args.poliigon_categories:
|
||||
path_categories = args.poliigon_categories
|
||||
else:
|
||||
print_debug(
|
||||
"Lacking path to Poliigon categories file in commandline "
|
||||
"arguments!")
|
||||
return None, None
|
||||
|
||||
return path_catalog, path_categories
|
||||
|
||||
|
||||
def main():
|
||||
print_debug("Hello Blender host, I am the client")
|
||||
|
||||
path_catalog, path_categories = command_line_args()
|
||||
has_catalog = path_catalog is not None
|
||||
has_categories = path_categories is not None
|
||||
if not has_catalog or not has_categories:
|
||||
print_debug("Missing catalog or categories path.")
|
||||
return
|
||||
|
||||
bpy.ops.preferences.addon_enable(module=ADDON_NAME)
|
||||
|
||||
bpy.ops.poliigon.asset_browser_sync_client(
|
||||
path_catalog=path_catalog, path_categories=path_categories)
|
||||
|
||||
print_debug("Subprocess exit")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
# #### 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 #####
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
|
||||
# FILE_H2C belongs to proc (proc.stdin)
|
||||
FILE_C2H = sys.stderr
|
||||
|
||||
|
||||
CMD_MARKER_START = "POLIIGON_CMD_START\n"
|
||||
CMD_MARKER_END = "POLIIGON_CMD_END\n"
|
||||
|
||||
|
||||
class SyncCmd(IntEnum):
|
||||
"""Command codes"""
|
||||
|
||||
HELLO = 0 # Client -> Host
|
||||
HELLO_OK = 1 # Host -> Client (ack)
|
||||
ASSET = 2 # Host -> Client
|
||||
ASSET_OK = 3 # Client -> Host (ack)
|
||||
ASSET_ERROR = 4 # Client -> Host (ack)
|
||||
EXIT = 5 # Host -> Client
|
||||
EXIT_ACK = 6 # Client -> Host (ack)
|
||||
CMD_DONE = 7 # internal
|
||||
CMD_ERROR = 8 # both directions (ack)
|
||||
STILL_THERE = 9 # Host -> Client
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncAssetBrowserCmd():
|
||||
"""Command to be transmitted between host and client"""
|
||||
|
||||
code: SyncCmd
|
||||
data: Optional[Dict] = None
|
||||
params: Optional[Dict] = None
|
||||
checksum: Optional[str] = ""
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, buf: str):
|
||||
"""Alternate constructor, used after receiving a command."""
|
||||
|
||||
cmd_dict = json.loads(buf)
|
||||
if "code" not in cmd_dict:
|
||||
raise KeyError("code")
|
||||
new = cls(**cmd_dict)
|
||||
new.code = SyncCmd(new.code)
|
||||
|
||||
cmd_is_ok = new.check_checksum()
|
||||
if not cmd_is_ok:
|
||||
raise RuntimeError("Checksum error")
|
||||
return new
|
||||
|
||||
def check_checksum(self) -> bool:
|
||||
checksum_to_test = self.checksum
|
||||
self.checksum = ""
|
||||
cmd_dict = asdict(self)
|
||||
json_str = json.dumps(cmd_dict, indent=4, default=vars) + "\n"
|
||||
checksum_calculated = hashlib.md5(json_str.encode("utf-8")).hexdigest()
|
||||
return checksum_to_test == checksum_calculated
|
||||
|
||||
def to_json(self) -> str:
|
||||
cmd_dict = asdict(self)
|
||||
json_str = json.dumps(cmd_dict, indent=4, default=vars) + "\n"
|
||||
return json_str
|
||||
|
||||
def calc_checksum(self) -> None:
|
||||
self.checksum = ""
|
||||
json_str = self.to_json()
|
||||
try:
|
||||
self.checksum = hashlib.md5(json_str.encode("utf-8")).hexdigest()
|
||||
except Exception as e:
|
||||
print(f"MD5 error: {e}")
|
||||
|
||||
def prepare_send(self) -> str:
|
||||
self.calc_checksum()
|
||||
json_str = CMD_MARKER_START
|
||||
json_str += self.to_json()
|
||||
json_str += CMD_MARKER_END
|
||||
return json_str
|
||||
|
||||
def send_to_process(self, proc) -> None: # use on host
|
||||
try:
|
||||
proc.stdin.write(self.prepare_send())
|
||||
proc.stdin.flush()
|
||||
except Exception:
|
||||
# Deliberately silencing exceptions here.
|
||||
# Any exceptions regarding unexpectedly closed handles are handled
|
||||
# in respective threads instead.
|
||||
pass
|
||||
|
||||
def send_to_stdio(self, file=FILE_C2H) -> None: # use on client
|
||||
file.write(self.prepare_send())
|
||||
file.flush()
|
||||
@@ -0,0 +1,273 @@
|
||||
# #### 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 #####
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from ..modules.poliigon_core.multilingual import _t
|
||||
from .asset_browser import (
|
||||
get_num_selected_assets,
|
||||
is_asset_browser,
|
||||
is_only_poliigon_selected,
|
||||
is_poliigon_library,
|
||||
t_status_bar_update)
|
||||
from ..constants import ASSET_ID_ALL
|
||||
from ..toolbox import get_context
|
||||
from .. import reporting
|
||||
|
||||
|
||||
def build_asset_browser_progress(ui: bpy.types.Panel,
|
||||
context: bpy.context,
|
||||
layout: Optional[bpy.types.UILayout] = None,
|
||||
show_label: bool = True,
|
||||
show_cancel: bool = True,
|
||||
show_second_line: bool = False) -> None:
|
||||
|
||||
if layout is None:
|
||||
layout = ui.layout
|
||||
|
||||
if cTB.num_asset_browser_jobs == 0 and not cTB.blender_client_starting:
|
||||
return
|
||||
elif cTB.blender_client_starting:
|
||||
col = layout.column()
|
||||
row = col.row(align=True)
|
||||
row.label(text=_t("Blender client starting up..."))
|
||||
return
|
||||
|
||||
num_jobs_done = cTB.num_jobs_error + cTB.num_jobs_ok
|
||||
progress = num_jobs_done / cTB.num_asset_browser_jobs
|
||||
progress = max(0.01, progress)
|
||||
done = num_jobs_done == cTB.num_asset_browser_jobs
|
||||
|
||||
layout.separator()
|
||||
|
||||
col = layout.column()
|
||||
row = col.row(align=True)
|
||||
|
||||
if show_label and done:
|
||||
row.label(text=_t("Asset Browser Synchronization : "))
|
||||
elif show_label and not done:
|
||||
row.label(text=_t("Asset Browser Synchronization : {0:.1%}").format(
|
||||
progress))
|
||||
|
||||
if done:
|
||||
if cTB.num_jobs_error:
|
||||
text = _t("Finished {0} assets, {1} errors").format(
|
||||
cTB.num_asset_browser_jobs, cTB.num_jobs_error)
|
||||
row.label(text=text,
|
||||
icon="ERROR")
|
||||
else:
|
||||
text = _t("Successfully finished {0} assets").format(
|
||||
cTB.num_asset_browser_jobs)
|
||||
row.label(text=text,
|
||||
icon="CHECKMARK")
|
||||
return
|
||||
|
||||
tooltip_progress = _t("Processing assets: {0} of {1} assets done").format(
|
||||
num_jobs_done, cTB.num_asset_browser_jobs)
|
||||
|
||||
split = row.split(factor=progress, align=True)
|
||||
op = split.operator(
|
||||
"poliigon.poliigon_setting", text="", emboss=1, depress=1
|
||||
)
|
||||
op.mode = "none"
|
||||
op.tooltip = tooltip_progress
|
||||
|
||||
op = split.operator(
|
||||
"poliigon.poliigon_setting", text="", emboss=1, depress=0
|
||||
)
|
||||
op.mode = "none"
|
||||
op.tooltip = tooltip_progress
|
||||
|
||||
if show_cancel:
|
||||
op = row.operator(
|
||||
"poliigon.cancel_asset_browser",
|
||||
text="",
|
||||
emboss=True,
|
||||
depress=0,
|
||||
icon="X"
|
||||
)
|
||||
if cTB.asset_browser_jobs_cancelled:
|
||||
row.enabled = False
|
||||
|
||||
if show_second_line:
|
||||
text = _t("Processed {0} of {1} assets.").format(
|
||||
num_jobs_done, cTB.num_asset_browser_jobs)
|
||||
col.label(text=text)
|
||||
|
||||
|
||||
class POLIIGON_PT_sidebar_left(bpy.types.Panel):
|
||||
bl_label = "Poliigon"
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
bl_region_type = "TOOLS"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
view_screen_tracked = False
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
if not cTB.is_logged_in():
|
||||
return False
|
||||
if not is_asset_browser(context):
|
||||
return False
|
||||
if not is_poliigon_library(context, incl_all_libs=False):
|
||||
return False
|
||||
return True
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
cTB._api._mp_relevant = True
|
||||
|
||||
if not self.view_screen_tracked:
|
||||
# TODO(patrick): value not retained, re-triggering on future draws
|
||||
self.view_screen_tracked = True
|
||||
cTB.track_screen("blend_browser_lib")
|
||||
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
|
||||
name_is_set = cTB.prefs.asset_browser_library_name != ""
|
||||
directory_is_set = cTB.get_library_path(primary=True) not in [None, ""]
|
||||
sync_options_enabled = name_is_set and directory_is_set
|
||||
|
||||
if cTB.num_asset_browser_jobs == 0 and not cTB.lock_client_start.locked():
|
||||
col.label(text="Poliigon assets:")
|
||||
row_manual_sync = col.row(align=1)
|
||||
op_manual_sync = row_manual_sync.operator(
|
||||
"poliigon.update_asset_browser",
|
||||
text=_t("Synchronize Local Assets"),
|
||||
emboss=True,
|
||||
icon="FILE_REFRESH",
|
||||
)
|
||||
op_manual_sync.asset_id = ASSET_ID_ALL
|
||||
row_manual_sync.enabled = sync_options_enabled
|
||||
else:
|
||||
col.label(text=_t("Poliigon Asset Browser Synchronization"))
|
||||
build_asset_browser_progress(self,
|
||||
context,
|
||||
col,
|
||||
show_label=False,
|
||||
show_second_line=True)
|
||||
|
||||
|
||||
class POLIIGON_PT_sidebar_right(bpy.types.Panel):
|
||||
bl_label = _t("Poliigon in Asset Browser")
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
bl_region_type = "TOOL_PROPS" # right side panel
|
||||
bl_options = {"HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
view_screen_tracked = False
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
if not cTB.is_logged_in():
|
||||
return False
|
||||
if not is_asset_browser(context):
|
||||
return False
|
||||
if not is_poliigon_library(context):
|
||||
return False
|
||||
if not is_only_poliigon_selected(context):
|
||||
return False
|
||||
return True
|
||||
|
||||
@reporting.handle_draw()
|
||||
def draw(self, context):
|
||||
if not is_poliigon_library(context):
|
||||
return
|
||||
|
||||
if not self.view_screen_tracked:
|
||||
cTB.track_screen("blend_browser_import")
|
||||
self.view_screen_tracked = True
|
||||
|
||||
num_selected = get_num_selected_assets(context)
|
||||
if num_selected == 1:
|
||||
label_import = _t("Import Asset (TODO)")
|
||||
label_reprocess = _t("Re-process Asset")
|
||||
elif num_selected > 1:
|
||||
label_import = _t("Import {0} Assets (TODO)").format(num_selected)
|
||||
label_reprocess = _t("Re-process {0} Assets").format(num_selected)
|
||||
else:
|
||||
label_import = _t("No Asset Selected")
|
||||
label_reprocess = _t("Re-process Asset")
|
||||
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.operator("poliigon.asset_browser_reprocess",
|
||||
text=label_reprocess,
|
||||
icon="FILE_REFRESH")
|
||||
|
||||
col.separator()
|
||||
|
||||
if not (cTB._env.env_name and "dev" in cTB._env.env_name.lower()):
|
||||
return
|
||||
|
||||
# TODO(Andreas): Import button currently in dev environment, only
|
||||
row = col.row(align=True)
|
||||
row.operator(
|
||||
"poliigon.asset_browser_import",
|
||||
text=label_import,
|
||||
emboss=True,
|
||||
)
|
||||
row.operator(
|
||||
"poliigon.asset_browser_quick_menu",
|
||||
text="",
|
||||
icon="TRIA_DOWN",
|
||||
)
|
||||
|
||||
|
||||
classes_prod = (
|
||||
POLIIGON_PT_sidebar_left,
|
||||
)
|
||||
|
||||
classes_dev = (
|
||||
POLIIGON_PT_sidebar_left,
|
||||
POLIIGON_PT_sidebar_right
|
||||
)
|
||||
|
||||
|
||||
classes = None
|
||||
cTB = None
|
||||
|
||||
|
||||
def register(addon_version: str):
|
||||
global classes
|
||||
global cTB
|
||||
|
||||
cTB = get_context(addon_version)
|
||||
|
||||
if cTB._env.env_name and "dev" in cTB._env.env_name.lower():
|
||||
classes = classes_dev
|
||||
else:
|
||||
classes = classes_prod
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.STATUSBAR_HT_header.prepend(build_asset_browser_progress)
|
||||
|
||||
|
||||
def unregister():
|
||||
if bpy.app.timers.is_registered(t_status_bar_update):
|
||||
bpy.app.timers.unregister(t_status_bar_update)
|
||||
|
||||
bpy.types.STATUSBAR_HT_header.remove(build_asset_browser_progress)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
Reference in New Issue
Block a user