Sketchfab integration, D321
Lets you use your sketchfab account from within Blender to upload models online.
This commit is contained in:
parent
20a4a567f9
commit
8ad356e332
|
@ -0,0 +1,466 @@
|
|||
# ##### 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 #####
|
||||
|
||||
bl_info = {
|
||||
"name": "Sketchfab Exporter",
|
||||
"author": "Bart Crouch",
|
||||
"version": (1, 2, 2),
|
||||
"blender": (2, 7, 0),
|
||||
"location": "Tools > Upload tab",
|
||||
"description": "Upload your model to Sketchfab",
|
||||
"warning": "",
|
||||
"wiki_url": "",
|
||||
"tracker_url": "",
|
||||
"category": "Import-Export"
|
||||
}
|
||||
|
||||
if "bpy" in locals():
|
||||
pass
|
||||
else:
|
||||
# uuid module causes an error messagebox on windows
|
||||
# - https://developer.blender.org/T38364
|
||||
# - https://developer.blender.org/T27666
|
||||
# using a dirty workaround to preload uuid without ctypes, until blender gets compiled with vs2012
|
||||
import platform
|
||||
if platform.system() == "Windows":
|
||||
import ctypes
|
||||
CDLL = ctypes.CDLL
|
||||
ctypes.CDLL = None
|
||||
import uuid
|
||||
ctypes.CDLL = CDLL
|
||||
del ctypes, CDLL
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import (StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
|
||||
SKETCHFAB_API_URL = "https://api.sketchfab.com"
|
||||
SKETCHFAB_API_MODELS_URL = SKETCHFAB_API_URL + "/v1/models"
|
||||
SKETCHFAB_API_TOKEN_URL = SKETCHFAB_API_URL + "/v1/users/claim-token"
|
||||
SKETCHFAB_MODEL_URL = "https://sketchfab.com/show/"
|
||||
SKETCHFAB_EXPORT_FILENAME = "sketchfab-export.blend"
|
||||
|
||||
_presets = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets")
|
||||
SKETCHFAB_PRESET_FILENAME = os.path.join(_presets, "sketchfab.txt")
|
||||
SKETCHFAB_EXPORT_DATA_FILE = os.path.join(_presets, "sketchfab-export-data.json")
|
||||
del _presets
|
||||
|
||||
|
||||
# Singleton for storing global state
|
||||
class _SketchfabState:
|
||||
__slots__ = (
|
||||
"uploading",
|
||||
"token_reload",
|
||||
"size_label",
|
||||
"model_url",
|
||||
|
||||
# store report args
|
||||
"report_message",
|
||||
"report_type",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.uploading = False
|
||||
self.token_reload = True
|
||||
self.size_label = ""
|
||||
self.model_url = ""
|
||||
|
||||
self.report_message = ""
|
||||
self.report_type = ''
|
||||
|
||||
sf_state = _SketchfabState()
|
||||
del _SketchfabState
|
||||
|
||||
# if True, no contact is made with the webserver
|
||||
DEBUG_MODE = False
|
||||
|
||||
|
||||
# change a bytes int into a properly formatted string
|
||||
def format_size(size):
|
||||
size /= 1024
|
||||
size_suffix = "kB"
|
||||
if size > 1024:
|
||||
size /= 1024
|
||||
size_suffix = "mB"
|
||||
if size >= 100:
|
||||
size = "%d" % int(size)
|
||||
else:
|
||||
size = "%.1f" % size
|
||||
size += " " + size_suffix
|
||||
|
||||
return size
|
||||
|
||||
|
||||
# attempt to load token from presets
|
||||
@persistent
|
||||
def load_token(dummy=False):
|
||||
filepath = SKETCHFAB_PRESET_FILENAME
|
||||
if not os.path.exists(filepath):
|
||||
return
|
||||
|
||||
token = ""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
token = f.readline()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
wm.sketchfab.token = token
|
||||
|
||||
|
||||
# save token to file
|
||||
def update_token(self, context):
|
||||
token = context.window_manager.sketchfab.token
|
||||
filepath = SKETCHFAB_PRESET_FILENAME
|
||||
|
||||
path = os.path.dirname(filepath)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(token)
|
||||
|
||||
|
||||
def upload_report(report_message, report_type):
|
||||
sf_state.report_message = report_message
|
||||
sf_state.report_type = report_type
|
||||
|
||||
|
||||
# upload the blend-file to sketchfab
|
||||
def upload(filepath, filename):
|
||||
import requests
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
props = wm.sketchfab
|
||||
|
||||
title = props.title
|
||||
if not title:
|
||||
title = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"description": props.description,
|
||||
"filename": filename,
|
||||
"tags": props.tags,
|
||||
"private": props.private,
|
||||
"token": props.token,
|
||||
"source": "blender-exporter",
|
||||
}
|
||||
|
||||
if props.private and props.password != "":
|
||||
data["password"] = props.password
|
||||
|
||||
files = {
|
||||
"fileModel": open(filepath, 'rb'),
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.post(SKETCHFAB_API_MODELS_URL, data=data, files=files, verify=False)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return upload_report("Upload failed. Error: %s" % str(e), 'WARNING')
|
||||
|
||||
result = r.json()
|
||||
if r.status_code != requests.codes.ok:
|
||||
return upload_report("Upload failed. Error: %s" % result["error"], 'WARNING')
|
||||
|
||||
sf_state.model_url = SKETCHFAB_MODEL_URL + result["result"]["id"]
|
||||
return upload_report("Upload complete. Available on your sketchfab.com dashboard.", 'INFO')
|
||||
|
||||
|
||||
# operator to export model to sketchfab
|
||||
class ExportSketchfab(bpy.types.Operator):
|
||||
"""Upload your model to Sketchfab"""
|
||||
bl_idname = "export.sketchfab"
|
||||
bl_label = "Upload"
|
||||
|
||||
_timer = None
|
||||
_thread = None
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == 'TIMER':
|
||||
if not self._thread.is_alive():
|
||||
wm = context.window_manager
|
||||
props = wm.sketchfab
|
||||
terminate(props.filepath)
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
# forward message from upload thread
|
||||
if not sf_state.report_type:
|
||||
sf_state.report_type = 'ERROR'
|
||||
self.report({sf_state.report_type}, sf_state.report_message)
|
||||
|
||||
wm.event_timer_remove(self._timer)
|
||||
self._thread.join()
|
||||
sf_state.uploading = False
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def execute(self, context):
|
||||
import json
|
||||
|
||||
if sf_state.uploading:
|
||||
self.report({'WARNING'}, "Please wait till current upload is finished")
|
||||
return {'CANCELLED'}
|
||||
|
||||
wm = context.window_manager
|
||||
sf_state.model_url = ""
|
||||
props = wm.sketchfab
|
||||
if not props.token:
|
||||
self.report({'ERROR'}, "Token is missing")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Prepare to save the file
|
||||
binary_path = bpy.app.binary_path
|
||||
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||
basename, ext = os.path.splitext(bpy.data.filepath)
|
||||
if not basename:
|
||||
basename = os.path.join(basename, "temp")
|
||||
if not ext:
|
||||
ext = ".blend"
|
||||
filepath = basename + "-export-sketchfab" + ext
|
||||
|
||||
try:
|
||||
# save a copy of actual scene but don't interfere with the users models
|
||||
bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=True, copy=True)
|
||||
|
||||
with open(SKETCHFAB_EXPORT_DATA_FILE, 'w') as s:
|
||||
json.dump({
|
||||
"models": props.models,
|
||||
"lamps": props.lamps,
|
||||
}, s)
|
||||
|
||||
subprocess.check_call([
|
||||
binary_path,
|
||||
"--background",
|
||||
"-noaudio",
|
||||
filepath,
|
||||
"--python", os.path.join(script_path, "pack_for_export.py"),
|
||||
])
|
||||
|
||||
os.remove(filepath)
|
||||
|
||||
# read subprocess call results
|
||||
with open(SKETCHFAB_EXPORT_DATA_FILE, 'r') as s:
|
||||
r = json.load(s)
|
||||
size = r["size"]
|
||||
props.filepath = r["filepath"]
|
||||
filename = r["filename"]
|
||||
|
||||
except Exception as e:
|
||||
self.report({'WARNING'}, "Error occured while preparing your file: %s" % str(e))
|
||||
return {'FINISHED'}
|
||||
|
||||
sf_state.uploading = True
|
||||
sf_state.size_label = format_size(size)
|
||||
self._thread = threading.Thread(
|
||||
target=upload,
|
||||
args=(props.filepath, filename),
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
wm.modal_handler_add(self)
|
||||
self._timer = wm.event_timer_add(1.0, context.window)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def cancel(self, context):
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
self._thread.join()
|
||||
|
||||
|
||||
# user interface
|
||||
class VIEW3D_PT_sketchfab(bpy.types.Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'TOOLS'
|
||||
bl_category = "Upload"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Sketchfab"
|
||||
|
||||
def draw(self, context):
|
||||
wm = context.window_manager
|
||||
props = wm.sketchfab
|
||||
if sf_state.token_reload:
|
||||
sf_state.token_reload = False
|
||||
if not props.token:
|
||||
load_token()
|
||||
layout = self.layout
|
||||
|
||||
layout.label("Export:")
|
||||
col = layout.box().column(align=True)
|
||||
col.prop(props, "models")
|
||||
col.prop(props, "lamps")
|
||||
|
||||
layout.label("Model info:")
|
||||
col = layout.box().column(align=True)
|
||||
col.prop(props, "title")
|
||||
col.prop(props, "description")
|
||||
col.prop(props, "tags")
|
||||
col.prop(props, "private")
|
||||
if props.private:
|
||||
col.prop(props, "password")
|
||||
|
||||
layout.label("Sketchfab account:")
|
||||
col = layout.box().column(align=True)
|
||||
col.prop(props, "token")
|
||||
row = col.row()
|
||||
row.operator("wm.sketchfab_email_token", text="Claim Your Token")
|
||||
row.alignment = 'RIGHT'
|
||||
if sf_state.uploading:
|
||||
layout.operator("export.sketchfab", text="Uploading %s" % sf_state.size_label)
|
||||
else:
|
||||
layout.operator("export.sketchfab")
|
||||
|
||||
model_url = sf_state.model_url
|
||||
if model_url:
|
||||
layout.operator("wm.url_open", text="View Online Model", icon='URL').url = model_url
|
||||
|
||||
|
||||
# property group containing all properties for the user interface
|
||||
class SketchfabProps(bpy.types.PropertyGroup):
|
||||
description = StringProperty(
|
||||
name="Description",
|
||||
description="Description of the model (optional)",
|
||||
default="")
|
||||
filepath = StringProperty(
|
||||
name="Filepath",
|
||||
description="internal use",
|
||||
default="",
|
||||
)
|
||||
lamps = EnumProperty(
|
||||
name="Lamps",
|
||||
items=(('ALL', "All", "Export all lamps in the file"),
|
||||
('NONE', "None", "Don't export any lamps"),
|
||||
('SELECTION', "Selection", "Only export selected lamps")),
|
||||
description="Determines which lamps are exported",
|
||||
default='ALL',
|
||||
)
|
||||
models = EnumProperty(
|
||||
name="Models",
|
||||
items=(('ALL', "All", "Export all meshes in the file"),
|
||||
('SELECTION', "Selection", "Only export selected meshes")),
|
||||
description="Determines which meshes are exported",
|
||||
default='SELECTION',
|
||||
)
|
||||
private = BoolProperty(
|
||||
name="Private",
|
||||
description="Upload as private (requires a pro account)",
|
||||
default=False,
|
||||
)
|
||||
password = StringProperty(
|
||||
name="Password",
|
||||
description="Password-protect your model (requires a pro account)",
|
||||
default="",
|
||||
)
|
||||
tags = StringProperty(
|
||||
name="Tags",
|
||||
description="List of tags, separated by spaces (optional)",
|
||||
default="",
|
||||
)
|
||||
title = StringProperty(
|
||||
name="Title",
|
||||
description="Title of the model (determined automatically if left empty)",
|
||||
default="",
|
||||
)
|
||||
token = StringProperty(
|
||||
name="Api Key",
|
||||
description="You can find this on your dashboard at the Sketchfab website",
|
||||
default="",
|
||||
update=update_token,
|
||||
)
|
||||
|
||||
|
||||
class SketchfabEmailToken(bpy.types.Operator):
|
||||
bl_idname = "wm.sketchfab_email_token"
|
||||
bl_label = "Enter your email to get a sketchfab token"
|
||||
|
||||
email = StringProperty(
|
||||
name="Email",
|
||||
default="you@example.com",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
import re
|
||||
import requests
|
||||
|
||||
EMAIL_RE = re.compile(r'[^@]+@[^@]+\.[^@]+')
|
||||
if not EMAIL_RE.match(self.email):
|
||||
self.report({'ERROR'}, "Wrong email format")
|
||||
try:
|
||||
r = requests.get(SKETCHFAB_API_TOKEN_URL + "?source=blender-exporter&email=" + self.email, verify=False)
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'FINISHED'}
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
self.report({'ERROR'}, "An error occured. Check the format of your email")
|
||||
else:
|
||||
self.report({'INFO'}, "Your email was sent at your email address")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=550)
|
||||
|
||||
|
||||
# remove file copy
|
||||
def terminate(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
# registration
|
||||
classes = (
|
||||
ExportSketchfab,
|
||||
SketchfabProps,
|
||||
SketchfabEmailToken,
|
||||
VIEW3D_PT_sketchfab,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.WindowManager.sketchfab = PointerProperty(
|
||||
type=SketchfabProps)
|
||||
|
||||
load_token()
|
||||
bpy.app.handlers.load_post.append(load_token)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
del bpy.types.WindowManager.sketchfab
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -0,0 +1,124 @@
|
|||
# ##### 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 #####
|
||||
|
||||
# This script is called from the sketchfab addon directly
|
||||
# to pack and save the file from a blender instance
|
||||
# so that the users file is left untouched.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
|
||||
|
||||
SKETCHFAB_EXPORT_DATA_FILENAME = 'sketchfab-export-data.json'
|
||||
|
||||
SKETCHFAB_EXPORT_DATA_FILE = os.path.join(
|
||||
bpy.utils.user_resource('SCRIPTS'),
|
||||
"presets",
|
||||
SKETCHFAB_EXPORT_DATA_FILENAME,
|
||||
)
|
||||
|
||||
|
||||
# save a copy of the current blendfile
|
||||
def save_blend_copy():
|
||||
import time
|
||||
|
||||
filepath = os.path.dirname(bpy.data.filepath)
|
||||
filename = time.strftime("Sketchfab_%Y_%m_%d_%H_%M_%S.blend",
|
||||
time.localtime(time.time()))
|
||||
filepath = os.path.join(filepath, filename)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=filepath,
|
||||
compress=True,
|
||||
copy=True)
|
||||
size = os.path.getsize(filepath)
|
||||
|
||||
return (filepath, filename, size)
|
||||
|
||||
|
||||
# change visibility statuses and pack images
|
||||
def prepare_assets(export_settings):
|
||||
hidden = set()
|
||||
images = set()
|
||||
if (export_settings['models'] == 'SELECTION' or
|
||||
export_settings['lamps'] != 'ALL'):
|
||||
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type == 'MESH':
|
||||
for mat_slot in ob.material_slots:
|
||||
if not mat_slot.material:
|
||||
continue
|
||||
for tex_slot in mat_slot.material.texture_slots:
|
||||
if not tex_slot:
|
||||
continue
|
||||
tex = tex_slot.texture
|
||||
if tex.type == 'IMAGE':
|
||||
image = tex.image
|
||||
if image is not None:
|
||||
images.add(image)
|
||||
if ((export_settings['models'] == 'SELECTION' and ob.type == 'MESH') or
|
||||
(export_settings['lamps'] == 'SELECTION' and ob.type == 'LAMP')):
|
||||
|
||||
if not ob.select and not ob.hide:
|
||||
ob.hide = True
|
||||
hidden.add(ob)
|
||||
elif export_settings['lamps'] == 'NONE' and ob.type == 'LAMP':
|
||||
if not ob.hide:
|
||||
ob.hide = True
|
||||
hidden.add(ob)
|
||||
|
||||
for img in images:
|
||||
if not img.packed_file:
|
||||
try:
|
||||
img.pack()
|
||||
except:
|
||||
# can fail in rare cases
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def prepare_file(export_settings):
|
||||
prepare_assets(export_settings)
|
||||
return save_blend_copy()
|
||||
|
||||
|
||||
def read_settings():
|
||||
with open(SKETCHFAB_EXPORT_DATA_FILE, 'r') as s:
|
||||
return json.load(s)
|
||||
|
||||
|
||||
def write_result(filepath, filename, size):
|
||||
with open(SKETCHFAB_EXPORT_DATA_FILE, 'w') as s:
|
||||
json.dump({
|
||||
'filepath': filepath,
|
||||
'filename': filename,
|
||||
'size': size,
|
||||
}, s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
export_settings = read_settings()
|
||||
filepath, filename, size = prepare_file(export_settings)
|
||||
write_result(filepath, filename, size)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
Loading…
Reference in New Issue