Merge branch 'master' into temp-xr-actions-D9124

This commit is contained in:
Peter Kim 2021-05-18 19:28:46 +09:00
commit 09f497a98b
20 changed files with 476 additions and 140 deletions

View File

@ -19,8 +19,8 @@
bl_info = {
"name": "BlenderKit Online Asset Library",
"author": "Vilem Duha, Petr Dlouhy",
"version": (2, 92, 0),
"blender": (2, 92, 0),
"version": (2, 93, 0),
"blender": (2, 93, 0),
"location": "View3D > Properties > BlenderKit",
"description": "Online BlenderKit library (materials, models, brushes and more). Connects to the internet.",
"warning": "",

View File

@ -426,8 +426,6 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
json_args=args_dict,
wait=False)
return {'FINISHED'}
start_thumbnailer(self, context)
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager

View File

@ -1278,6 +1278,9 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
description="",
default="")
# close_window: BoolProperty(name='Close window',
# description='Try to close the window below mouse before download',
# default=False)
# @classmethod
# def poll(cls, context):
# return bpy.context.window_manager.BlenderKitModelThumbnails is not ''
@ -1376,6 +1379,9 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
layout.prop(self, 'resolution', expand=True, icon_only=False)
def invoke(self, context, event):
# if self.close_window:
# context.window.cursor_warp(event.mouse_x-1000, event.mouse_y - 1000);
print(self.asset_base_id)
wm = context.window_manager
# only make a pop up in case of switching resolutions
@ -1393,6 +1399,15 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
self.resolution = 'ORIGINAL'
return wm.invoke_props_dialog(self)
# if self.close_window:
# time.sleep(0.1)
# context.area.tag_redraw()
# time.sleep(0.1)
#
# context.window.cursor_warp(event.mouse_x, event.mouse_y);
return self.execute(context)
def register_download():
bpy.utils.register_class(BlenderkitDownloadOperator)

View File

@ -16,7 +16,7 @@
#
# ##### END GPL LICENSE BLOCK #####
from blenderkit import paths, utils, rerequests, tasks_queue, ratings_utils
from blenderkit import paths, utils, rerequests, tasks_queue, ratings_utils, icons
import bpy
import requests, threading
@ -176,44 +176,70 @@ class UploadRatingOperator(bpy.types.Operator):
def draw_ratings_menu(self, context, layout):
pcoll = icons.icon_collections["main"]
profile_name = ''
profile = bpy.context.window_manager.get('bkit profile')
if profile:
profile_name = ' ' + profile['user']['firstName']
col = layout.column()
# layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0)
row = col.row()
row.label(text='Quality:', icon = 'SOLO_ON')
row = col.row()
row.label(text='Please help the community by rating quality:')
row = col.row()
row.prop(self, 'rating_quality_ui', expand=True, icon_only=True, emboss=False)
if self.rating_quality>0:
# row = col.row()
row.label(text=f' Thanks{profile_name}!', icon = 'FUND')
# row.label(text=str(self.rating_quality))
col.separator()
col.separator()
row = layout.row()
row = col.row()
row.label(text='Complexity:', icon_value=pcoll['dumbbell'].icon_id)
row = col.row()
row.label(text=f"How many hours did this {self.asset_type} save you?")
if utils.profile_is_validator():
row = col.row()
row.prop(self, 'rating_work_hours')
if self.asset_type in ('model', 'scene'):
row = layout.row()
if utils.profile_is_validator():
col.prop(self, 'rating_work_hours')
row = col.row()
row.prop(self, 'rating_work_hours_ui', expand=True, icon_only=False, emboss=True)
if float(self.rating_work_hours_ui) > 100:
utils.label_multiline(layout,
utils.label_multiline(col,
text=f"\nThat's huge! please be sure to give such rating only to godly {self.asset_type}s.\n",
width=500)
elif float(self.rating_work_hours_ui) > 18:
layout.separator()
col.separator()
utils.label_multiline(layout,
utils.label_multiline(col,
text=f"\nThat's a lot! please be sure to give such rating only to amazing {self.asset_type}s.\n",
width=500)
elif self.asset_type == 'hdr':
row = layout.row()
row = col.row()
row.prop(self, 'rating_work_hours_ui_1_10', expand=True, icon_only=False, emboss=True)
else:
row = layout.row()
row = col.row()
row.prop(self, 'rating_work_hours_ui_1_5', expand=True, icon_only=False, emboss=True)
if self.rating_work_hours>0:
row = col.row()
row.label(text=f'Thanks{profile_name}, you are amazing!', icon='FUND')
class FastRateMenu(Operator, ratings_utils.RatingsProperties):
"""Rating of the assets , also directly from the asset bar - without need to download assets"""
bl_idname = "wm.blenderkit_menu_rating_upload"
bl_label = "Rate asset"
bl_label = ""
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -226,6 +252,7 @@ class FastRateMenu(Operator, ratings_utils.RatingsProperties):
def draw(self, context):
layout = self.layout
layout.label(text=self.message)
layout.separator()
draw_ratings_menu(self, context, layout)
@ -265,8 +292,9 @@ class FastRateMenu(Operator, ratings_utils.RatingsProperties):
if self.asset_id == '':
return {'CANCELLED'}
self.message = f"Rate asset {self.asset_name}"
self.message = f"{self.asset_name}"
wm = context.window_manager
self.prefill_ratings()
if self.asset_type in ('model', 'scene'):
# spawn a wider one for validators for the enum buttons

View File

@ -30,7 +30,6 @@ from bpy.props import (
PointerProperty,
)
import threading
import requests
import logging
@ -69,21 +68,43 @@ def send_rating_to_thread_work_hours(url, ratings, headers):
thread.start()
def store_rating_local(asset_id, type='quality', value=0):
context = bpy.context
context.window_manager['asset ratings'] = context.window_manager.get('asset ratings', {})
context.window_manager['asset ratings'][asset_id] = context.window_manager['asset ratings'].get(asset_id, {})
context.window_manager['asset ratings'][asset_id][type] = value
def get_rating_local(asset_id):
context = bpy.context
context.window_manager['asset ratings'] = context.window_manager.get('asset ratings', {})
rating = context.window_manager['asset ratings'].get(asset_id)
if rating:
return rating.to_dict()
return None
def update_ratings_quality(self, context):
user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
api_key = user_preferences.api_key
headers = utils.get_headers(api_key)
asset = self.id_data
if asset:
if not (hasattr(self, 'rating_quality')):
# first option is for rating of assets that are from scene
asset = self.id_data
bkit_ratings = asset.bkit_ratings
url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/'
asset_id = asset['asset_data']['id']
else:
# this part is for operator rating:
bkit_ratings = self
url = paths.get_api_url() + f'assets/{self.asset_id}/rating/'
asset_id = self.asset_id
if bkit_ratings.rating_quality > 0.1:
url = paths.get_api_url() + f'assets/{asset_id}/rating/'
store_rating_local(asset_id, type='quality', value=bkit_ratings.rating_quality)
ratings = [('quality', bkit_ratings.rating_quality)]
tasks_queue.add_task((send_rating_to_thread_quality, (url, ratings, headers)), wait=2.5, only_last=True)
@ -92,16 +113,21 @@ def update_ratings_work_hours(self, context):
user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
api_key = user_preferences.api_key
headers = utils.get_headers(api_key)
asset = self.id_data
if asset:
if not (hasattr(self, 'rating_work_hours')):
# first option is for rating of assets that are from scene
asset = self.id_data
bkit_ratings = asset.bkit_ratings
url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/'
asset_id = asset['asset_data']['id']
else:
# this part is for operator rating:
bkit_ratings = self
url = paths.get_api_url() + f'assets/{self.asset_id}/rating/'
asset_id = self.asset_id
if bkit_ratings.rating_work_hours > 0.45:
url = paths.get_api_url() + f'assets/{asset_id}/rating/'
store_rating_local(asset_id, type='working_hours', value=bkit_ratings.rating_work_hours)
ratings = [('working_hours', round(bkit_ratings.rating_work_hours, 1))]
tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=2.5, only_last=True)
@ -140,8 +166,6 @@ def update_ratings_work_hours_ui_1_5(self, context):
bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT',
message='Please login/signup to rate assets. Clicking OK takes you to web login.')
# self.rating_work_hours_ui_1_5 = '0'
# print('updating 1-5')
# print(float(self.rating_work_hours_ui_1_5))
self.rating_work_hours = float(self.rating_work_hours_ui_1_5)
@ -171,6 +195,7 @@ def stars_enum_callback(self, context):
items.append((f'{a + 1}', f'{a + 1}', '', icon, a + 1))
return items
class RatingsProperties():
message: StringProperty(
name="message",
@ -279,4 +304,17 @@ class RatingsProperties():
default='0',
update=update_ratings_work_hours_ui_1_10,
options={'SKIP_SAVE'}
)
)
def prefill_ratings(self):
# pre-fill ratings
ratings = get_rating_local(self.asset_id)
if ratings and ratings.get('quality'):
self.rating_quality = ratings['quality']
if ratings and ratings.get('working_hours'):
wh = int(ratings['working_hours'])
self.rating_work_hours_ui = str(wh)
if wh < 6:
self.rating_work_hours_ui_1_5 = str(int(ratings['working_hours']))
if wh < 11:
self.rating_work_hours_ui_1_10 = str(int(ratings['working_hours']))

View File

@ -628,13 +628,14 @@ class ThumbDownloader(threading.Thread):
return self._stop_event.is_set()
def run(self):
print('thumb downloader', self.url)
# print('thumb downloader', self.url)
r = None
try:
r = requests.get(self.url, stream=False)
except Exception as e:
bk_logger.error('Thumbnail download failed')
bk_logger.error(str(e))
if r.status_code == 200:
if r and r.status_code == 200:
with open(self.path, 'wb') as f:
f.write(r.content)
# ORIGINALLY WE DOWNLOADED THUMBNAILS AS STREAM, BUT THIS WAS TOO SLOW.
@ -1405,6 +1406,11 @@ class SearchOperator(Operator):
options={'SKIP_SAVE'}
)
# close_window: BoolProperty(name='Close window',
# description='Try to close the window below mouse before download',
# default=False)
tooltip: bpy.props.StringProperty(default='Runs search and displays the asset bar at the same time')
@classmethod
@ -1428,6 +1434,13 @@ class SearchOperator(Operator):
return {'FINISHED'}
# def invoke(self, context, event):
# if self.close_window:
# context.window.cursor_warp(event.mouse_x, event.mouse_y - 100);
# context.area.tag_redraw()
#
# context.window.cursor_warp(event.mouse_x, event.mouse_y);
# return self. execute(context)
class UrlOperator(Operator):
""""""

View File

@ -382,6 +382,7 @@ def draw_tooltip_with_author(asset_data, x, y):
gimg = utils.get_hidden_image(a['gravatarImg'], a['gravatarHash'])
aname = asset_data['displayName']
aname = aname[0].upper() + aname[1:]
if len(aname)>36:
aname = f"{aname[:33]}..."
@ -1419,10 +1420,10 @@ class AssetBarOperator(bpy.types.Operator):
my = event.mouse_y - r.y
if event.value == 'PRESS' and mouse_in_asset_bar(mx, my):
context.window.cursor_warp(event.mouse_x - 400, event.mouse_y - 20);
# context.window.cursor_warp(event.mouse_x - 300, event.mouse_y - 10);
bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT')
context.window.cursor_warp(event.mouse_x, event.mouse_y);
# context.window.cursor_warp(event.mouse_x, event.mouse_y);
# bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_asset_menu')
return {'RUNNING_MODAL'}

View File

@ -421,8 +421,6 @@ class VIEW3D_PT_blenderkit_model_properties(Panel):
layout.label(text=str(ad['name']))
if o.instance_type == 'COLLECTION' and o.instance_collection is not None:
layout.operator('object.blenderkit_bring_to_scene', text='Bring to scene')
# layout.label(text='Ratings:')
# draw_panel_model_rating(self, context)
layout.label(text='Asset tools:')
draw_asset_context_menu(self.layout, context, ad, from_panel=True)
@ -465,8 +463,6 @@ class NODE_PT_blenderkit_material_properties(Panel):
if m.get('asset_data') is not None:
ad = m['asset_data']
layout.label(text=str(ad['name']))
layout.label(text='Ratings:')
draw_panel_material_ratings(self, context)
layout.label(text='Asset tools:')
draw_asset_context_menu(self.layout, context, ad, from_panel=True)
@ -1390,6 +1386,7 @@ def label_or_url(layout, text='', tooltip='', url='', icon_value=None, icon=None
else:
op = layout.operator('wm.blenderkit_tooltip', text=text)
op.tooltip = tooltip
# these are here to move the text to left, since operators can only center text by default
layout.label(text='')
layout.label(text='')
return
@ -1406,15 +1403,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
bl_idname = "wm.blenderkit_asset_popup"
bl_label = "BlenderKit asset popup"
width = 700
width = 800
@classmethod
def poll(cls, context):
return True
def draw_menu(self, context, layout):
col = layout.column()
draw_asset_context_menu(col, context, self.asset_data, from_panel=False)
# layout = layout.column()
draw_asset_context_menu(layout, context, self.asset_data, from_panel=False)
def draw_property(self, layout, left, right, icon=None, icon_value=None, url='', tooltip=''):
right = str(right)
@ -1439,8 +1436,19 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
parameter = utils.get_param(self.asset_data, key)
if parameter == None:
return
if type(parameter) == int:
parameter = f"{parameter:,d}"
elif type(parameter) == float:
parameter = f"{parameter:,.1f}"
self.draw_property(layout, pretext, parameter)
def draw_description(self, layout, width = 250):
if len(self.asset_data['description']) > 0:
box = layout.box()
box.scale_y = 0.8
box.label(text='Description')
utils.label_multiline(box, self.asset_data['description'], width=width)
def draw_properties(self, layout, width=250):
if type(self.asset_data['parameters']) == list:
@ -1448,20 +1456,14 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
else:
mparams = self.asset_data['parameters']
layout = layout.column()
if len(self.asset_data['description']) > 0:
box = layout.box()
box.scale_y = 0.8
box.label(text='Description')
utils.label_multiline(box, self.asset_data['description'], width=width)
pcoll = icons.icon_collections["main"]
box = layout.box()
box.scale_y = 0.8
box.label(text='Properties')
if self.asset_data.get('license') == 'cc_zero':
t = 'CC Zero'
t = 'CC Zero '
icon = pcoll['cc0']
else:
@ -1508,29 +1510,32 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
)
# resolution/s
# fs = self.asset_data['files']
#
# if fs and len(fs) > 2:
# resolutions = ''
# list.sort(fs, key=lambda f: f['fileType'])
# for f in fs:
# if f['fileType'].find('resolution') > -1:
# resolutions += f['fileType'][11:] + ' '
# resolutions = resolutions.replace('_', '.')
# self.draw_property(box, 'Resolutions:', resolutions)
resolution = utils.get_param(self.asset_data, 'textureResolutionMax')
if resolution is not None:
fs = self.asset_data['files']
ress = f"{int(round(resolution / 1024, 0))}K"
self.draw_property(box, 'Resolution', ress,
tooltip='Maximal resolution of textures in this asset.\n' \
'Most texture asset have also lower resolutions generated.\n' \
'Go to BlenderKit add-on import settings to set default resolution')
if fs and len(fs) > 2 and utils.profile_is_validator():
resolutions = ''
list.sort(fs, key=lambda f: f['fileType'])
for f in fs:
if f['fileType'].find('resolution') > -1:
resolutions += f['fileType'][11:] + ' '
resolutions = resolutions.replace('_', '.')
self.draw_property(box, 'Generated:', resolutions)
self.draw_asset_parameter(box, key='designer', pretext='Designer')
self.draw_asset_parameter(box, key='manufacturer', pretext='Manufacturer') # TODO make them clickable!
self.draw_asset_parameter(box, key='designCollection', pretext='Collection')
self.draw_asset_parameter(box, key='designVariant', pretext='Variant')
self.draw_asset_parameter(box, key='designYear', pretext='Design year')
self.draw_asset_parameter(box, key='faceCount', pretext='Face count')
# self.draw_asset_parameter(box, key='thumbnailScale', pretext='Preview scale')
# self.draw_asset_parameter(box, key='purePbr', pretext='Pure PBR')
@ -1544,6 +1549,14 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
utils.fmt_length(mparams['dimensionY']),
utils.fmt_length(mparams['dimensionZ']))
self.draw_property(box, 'Size:', t)
if self.asset_data.get('filesSize'):
fs = self.asset_data['filesSize']
fsmb = fs // (1024 * 1024)
fskb = fs % 1024
if fsmb == 0:
self.draw_property(box, 'Original size:', f'{fskb}KB')
else:
self.draw_property(box, 'Original size:', f'{fsmb}MB')
# Tags section
# row = box.row()
# letters_on_row = 0
@ -1597,6 +1610,10 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
icon_value=icon.icon_id,
tooltip=plans_tooltip,
url=plans_link)
if utils.profile_is_validator():
date = self.asset_data['created'][:10]
date = f"{date[8:10]}. {date[5:7]}. {date[:4]}"
self.draw_property(box, 'Created:', date)
def draw_author_area(self, context, layout, width=330):
self.draw_author(context, layout, width=width)
@ -1627,7 +1644,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
utils.label_multiline(col, text=a['tooltip'], width=text_width)
# check if author didn't fill any data about himself and prompt him if that's the case
if upload.user_is_owner(asset_data=self.asset_data) and a.get('aboutMe') is not None and len(
if utils.user_is_owner(asset_data=self.asset_data) and a.get('aboutMe') is not None and len(
a.get('aboutMe', '')) == 0:
row = col.row()
row.enabled = False
@ -1656,14 +1673,13 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
op.keywords = ''
op.author_id = self.asset_data['author']['id']
def draw_thumbnail_box(self, layout):
def draw_thumbnail_box(self, layout, width = 250):
layout.emboss = 'NORMAL'
box_thumbnail = layout.box()
box_thumbnail.scale_y = .4
box_thumbnail.template_icon(icon_value=self.img.preview.icon_id, scale=34.0)
box_thumbnail.template_icon(icon_value=self.img.preview.icon_id, scale=width*.12)
# row = box_thumbnail.row()
# row.scale_y = 3
@ -1721,16 +1737,36 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
# left - tooltip & params
row = box.row()
split_factor = 0.7
split_left_left = row.split(factor=split_factor)
self.draw_properties(split_left_left, width=int(width * split_factor))
split_left = row.split(factor=split_factor)
col = split_left.column()
width_left = int(width * split_factor)
self.draw_description(col, width=width_left)
self.draw_properties(col, width=width_left)
# right - menu
col1 = split_left_left.split()
self.draw_menu(context, col1)
split_right = split_left.split()
col = split_right.column()
self.draw_menu(context, col)
# author
self.draw_author_area(context, box, width=width)
# self.draw_author_area(context, box, width=width)
#
# col = box.column_flow(columns=2)
# self.draw_menu(context, col)
#
#
# # self.draw_description(box, width=int(width))
# self.draw_properties(box, width=int(width))
def draw(self, context):
ui_props = context.scene.blenderkitUI
@ -1741,23 +1777,29 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
# top draggabe bar with name of the asset
top_row = layout.row()
top_drag_bar = top_row.box()
top_drag_bar.label(text=asset_data['displayName'])
aname = asset_data['displayName']
aname = aname[0].upper() + aname[1:]
top_drag_bar.label(text=aname)
# left side
row = layout.row(align=True)
split_ratio = 0.5
split_left = row.split(factor=0.5)
self.draw_thumbnail_box(split_left)
split_ratio = 0.45
split_left = row.split(factor=split_ratio)
left_column = split_left.column()
self.draw_thumbnail_box(left_column, width = int(self.width * split_ratio))
# self.draw_description(left_column, width = int(self.width*split_ratio))
# right split
split_right = split_left.split()
self.draw_menu_desc_author(context, split_right, width=int(self.width * split_ratio))
self.draw_menu_desc_author(context, split_right, width=int(self.width * (1-split_ratio)))
if not utils.user_is_owner(asset_data=asset_data):
#Draw ratings, but not for owners of assets - doesn't make sense.
ratings_box = layout.box()
ratings.draw_ratings_menu(self, context, ratings_box)
# else:
# ratings_box.label('Here you should find ratings, but you can not rate your own assets ;)')
ratings_box = layout.box()
ratings_box.scale_y = 0.7
ratings_box.label(text='Rate asset quality:')
ratings.draw_ratings_menu(self, context, ratings_box)
tip_box = layout.box()
tip_box.label(text=self.tip)
@ -1781,6 +1823,10 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
bl_label = asset_data['name']
self.tip = search.get_random_tip()
self.tip = self.tip.replace('\n', '')
# pre-fill ratings
self.prefill_ratings()
return wm.invoke_popup(self, width=self.width)

View File

@ -567,14 +567,6 @@ def update_free_full(self, context):
" based on our fair share system. " \
"Part of subscription is sent to artists based on usage by paying users.")
def user_is_owner(asset_data=None):
'''Checks if the current logged in user is owner of the asset'''
profile = bpy.context.window_manager.get('bkit profile')
if profile is None:
return False
if int(asset_data['author']['id']) == int(profile['user']['id']):
return True
return False
def can_edit_asset(active_index=-1, asset_data=None):
if active_index < 0 and not asset_data:

View File

@ -784,6 +784,14 @@ def profile_is_validator():
return True
return False
def user_is_owner(asset_data=None):
'''Checks if the current logged in user is owner of the asset'''
profile = bpy.context.window_manager.get('bkit profile')
if profile is None:
return False
if int(asset_data['author']['id']) == int(profile['user']['id']):
return True
return False
def guard_from_crash():
'''

View File

@ -15,7 +15,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (1, 7, 5),
"version": (1, 7, 7),
'blender': (2, 91, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',
@ -298,6 +298,24 @@ class ExportGLTF2_Base:
default=False
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=False
)
use_renderable: BoolProperty(
name='Renderable Objects',
description='Export renderable objects only',
default=False
)
use_active_collection: BoolProperty(
name='Active Collection',
description='Export objects in the active collection only',
default=False
)
export_extras: BoolProperty(
name='Custom Properties',
description='Export custom properties as glTF extras',
@ -464,6 +482,9 @@ class ExportGLTF2_Base:
exceptional = [
# options that don't start with 'export_'
'use_selection',
'use_visible',
'use_renderable',
'use_active_collection',
'use_mesh_edges',
'use_mesh_vertices',
]
@ -528,6 +549,10 @@ class ExportGLTF2_Base:
else:
export_settings['gltf_selected'] = self.use_selection
export_settings['gltf_visible'] = self.use_visible
export_settings['gltf_renderable'] = self.use_renderable
export_settings['gltf_active_collection'] = self.use_active_collection
# export_settings['gltf_selected'] = self.use_selection This can be uncomment when removing compatibility of export_selected
export_settings['gltf_layers'] = True # self.export_layers
export_settings['gltf_extras'] = self.export_extras
@ -657,6 +682,9 @@ class GLTF_PT_export_include(bpy.types.Panel):
col = layout.column(heading = "Limit to", align = True)
col.prop(operator, 'use_selection')
col.prop(operator, 'use_visible')
col.prop(operator, 'use_renderable')
col.prop(operator, 'use_active_collection')
col = layout.column(heading = "Data", align = True)
col.prop(operator, 'export_extras')

View File

@ -26,6 +26,9 @@ FILTERED_CAMERAS = 'filtered_cameras'
APPLY = 'gltf_apply'
SELECTED = 'gltf_selected'
VISIBLE = 'gltf_visible'
RENDERABLE = 'gltf_renderable'
ACTIVE_COLLECTION = 'gltf_active_collection'
SKINS = 'gltf_skins'
DISPLACEMENT = 'gltf_displacement'
FORCE_SAMPLING = 'gltf_force_sampling'

View File

@ -127,6 +127,19 @@ def __filter_node(blender_object, blender_scene, export_settings):
if export_settings[gltf2_blender_export_keys.SELECTED] and blender_object.select_get() is False:
return False
if export_settings[gltf2_blender_export_keys.VISIBLE] and blender_object.visible_get() is False:
return False
# render_get() doesn't exist, so unfortunately this won't take into account the Collection settings
if export_settings[gltf2_blender_export_keys.RENDERABLE] and blender_object.hide_render is True:
return False
if export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]:
found = any(x == blender_object for x in bpy.context.collection.all_objects)
if not found:
return False
return True
@ -357,6 +370,10 @@ def __gather_mesh_from_nonmesh(blender_object, library, export_settings):
blender_mesh_owner = blender_object
blender_mesh = blender_mesh_owner.to_mesh()
# In some cases (for example curve with single vertice), no blender_mesh is created (without crash)
if blender_mesh is None:
return None
except Exception:
return None

View File

@ -21,7 +21,7 @@
bl_info = {
"name": "3D-Print Toolbox",
"author": "Campbell Barton",
"blender": (2, 82, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Utilities for 3D printing",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/3d_print_toolbox.html",
@ -34,7 +34,8 @@ if "bpy" in locals():
import importlib
importlib.reload(ui)
importlib.reload(operators)
importlib.reload(mesh_helpers)
if "mesh_helpers" in locals():
importlib.reload(mesh_helpers)
if "export" in locals():
importlib.reload(export)
else:

View File

@ -31,32 +31,62 @@ from bpy.props import (
)
import bmesh
from . import (
mesh_helpers,
report,
)
from . import report
def clean_float(text):
# strip trailing zeros: 0.000 -> 0.0
def clean_float(value: float, precision: int = 0) -> str:
# Avoid scientific notation and strip trailing zeros: 0.000 -> 0.0
text = f"{value:.{precision}f}"
index = text.rfind(".")
if index != -1:
index += 2
head, tail = text[:index], text[index:]
tail = tail.rstrip("0")
text = head + tail
return text
def get_unit(unit_system: str, unit: str) -> tuple[float, str]:
# Returns unit length relative to meter and unit symbol
units = {
"METRIC": {
"KILOMETERS": (1000.0, "km"),
"METERS": (1.0, "m"),
"CENTIMETERS": (0.01, "cm"),
"MILLIMETERS": (0.001, "mm"),
"MICROMETERS": (0.000001, "µm"),
},
"IMPERIAL": {
"MILES": (1609.344, "mi"),
"FEET": (0.3048, "\'"),
"INCHES": (0.0254, "\""),
"THOU": (0.0000254, "thou"),
},
}
try:
return units[unit_system][unit]
except KeyError:
fallback_unit = "CENTIMETERS" if unit_system == "METRIC" else "INCHES"
return units[unit_system][fallback_unit]
# ---------
# Mesh Info
class MESH_OT_print3d_info_volume(Operator):
bl_idname = "mesh.print3d_info_volume"
bl_label = "3D-Print Info Volume"
bl_description = "Report the volume of the active mesh"
def execute(self, context):
from . import mesh_helpers
scene = context.scene
unit = scene.unit_settings
scale = 1.0 if unit.system == 'NONE' else unit.scale_length
@ -66,14 +96,14 @@ class MESH_OT_print3d_info_volume(Operator):
volume = bm.calc_volume()
bm.free()
if unit.system == 'METRIC':
volume_cm = volume * (scale ** 3.0) / (0.01 ** 3.0)
volume_fmt = "{} cm".format(clean_float(f"{volume_cm:.4f}"))
elif unit.system == 'IMPERIAL':
volume_inch = volume * (scale ** 3.0) / (0.0254 ** 3.0)
volume_fmt = '{} "'.format(clean_float(f"{volume_inch:.4f}"))
if unit.system == 'NONE':
volume_fmt = clean_float(volume, 8)
else:
volume_fmt = clean_float(f"{volume:.8f}")
length, symbol = get_unit(unit.system, unit.length_unit)
volume_unit = volume * (scale ** 3.0) / (length ** 3.0)
volume_str = clean_float(volume_unit, 4)
volume_fmt = f"{volume_str} {symbol}"
report.update((f"Volume: {volume_fmt}³", None))
@ -86,6 +116,8 @@ class MESH_OT_print3d_info_area(Operator):
bl_description = "Report the surface area of the active mesh"
def execute(self, context):
from . import mesh_helpers
scene = context.scene
unit = scene.unit_settings
scale = 1.0 if unit.system == 'NONE' else unit.scale_length
@ -95,14 +127,14 @@ class MESH_OT_print3d_info_area(Operator):
area = mesh_helpers.bmesh_calc_area(bm)
bm.free()
if unit.system == 'METRIC':
area_cm = area * (scale ** 2.0) / (0.01 ** 2.0)
area_fmt = "{} cm".format(clean_float(f"{area_cm:.4f}"))
elif unit.system == 'IMPERIAL':
area_inch = area * (scale ** 2.0) / (0.0254 ** 2.0)
area_fmt = '{} "'.format(clean_float(f"{area_inch:.4f}"))
if unit.system == 'NONE':
area_fmt = clean_float(area, 8)
else:
area_fmt = clean_float(f"{area:.8f}")
length, symbol = get_unit(unit.system, unit.length_unit)
area_unit = area * (scale ** 2.0) / (length ** 2.0)
area_str = clean_float(area_unit, 4)
area_fmt = f"{area_str} {symbol}"
report.update((f"Area: {area_fmt}²", None))
@ -137,6 +169,7 @@ class MESH_OT_print3d_check_solid(Operator):
@staticmethod
def main_check(obj, info):
import array
from . import mesh_helpers
bm = mesh_helpers.bmesh_copy_from_object(obj, transform=False, triangulate=False)
@ -162,6 +195,8 @@ class MESH_OT_print3d_check_intersections(Operator):
@staticmethod
def main_check(obj, info):
from . import mesh_helpers
faces_intersect = mesh_helpers.bmesh_check_self_intersect_object(obj)
info.append((f"Intersect Face: {len(faces_intersect)}", (bmesh.types.BMFace, faces_intersect)))
@ -180,6 +215,7 @@ class MESH_OT_print3d_check_degenerate(Operator):
@staticmethod
def main_check(obj, info):
import array
from . import mesh_helpers
scene = bpy.context.scene
print_3d = scene.print_3d
@ -207,6 +243,7 @@ class MESH_OT_print3d_check_distorted(Operator):
@staticmethod
def main_check(obj, info):
import array
from . import mesh_helpers
scene = bpy.context.scene
print_3d = scene.print_3d
@ -238,6 +275,8 @@ class MESH_OT_print3d_check_thick(Operator):
@staticmethod
def main_check(obj, info):
from . import mesh_helpers
scene = bpy.context.scene
print_3d = scene.print_3d
@ -255,6 +294,8 @@ class MESH_OT_print3d_check_sharp(Operator):
@staticmethod
def main_check(obj, info):
from . import mesh_helpers
scene = bpy.context.scene
print_3d = scene.print_3d
angle_sharp = print_3d.angle_sharp
@ -282,6 +323,7 @@ class MESH_OT_print3d_check_overhang(Operator):
@staticmethod
def main_check(obj, info):
from mathutils import Vector
from . import mesh_helpers
scene = bpy.context.scene
print_3d = scene.print_3d
@ -355,6 +397,8 @@ class MESH_OT_print3d_clean_distorted(Operator):
)
def execute(self, context):
from . import mesh_helpers
obj = context.active_object
bm = mesh_helpers.bmesh_from_object(obj)
bm.normal_update()
@ -589,7 +633,7 @@ def _scale(scale, report=None, report_suffix=""):
if scale != 1.0:
bpy.ops.transform.resize(value=(scale,) * 3)
if report is not None:
scale_fmt = clean_float(f"{scale:.6f}")
scale_fmt = clean_float(scale, 6)
report({'INFO'}, f"Scaled by {scale_fmt}{report_suffix}")
@ -611,7 +655,7 @@ class MESH_OT_print3d_scale_to_volume(Operator):
def execute(self, context):
scale = math.pow(self.volume, 1 / 3) / math.pow(self.volume_init, 1 / 3)
scale_fmt = clean_float(f"{scale:.6f}")
scale_fmt = clean_float(scale, 6)
self.report({'INFO'}, f"Scaled by {scale_fmt}")
_scale(scale, self.report)
return {'FINISHED'}
@ -619,6 +663,8 @@ class MESH_OT_print3d_scale_to_volume(Operator):
def invoke(self, context, event):
def calc_volume(obj):
from . import mesh_helpers
bm = mesh_helpers.bmesh_copy_from_object(obj, apply_modifiers=True)
volume = bm.calc_volume(signed=True)
bm.free()

View File

@ -29,8 +29,8 @@
bl_info = {
"name": "Precision Drawing Tools (PDT)",
"author": "Alan Odom (Clockmender), Rune Morling (ermo)",
"version": (1, 4, 0),
"blender": (2, 83, 0),
"version": (1, 5, 1),
"blender": (2, 90, 0),
"location": "View3D > UI > PDT",
"description": "Precision Drawing Tools for Acccurate Modelling",
"warning": "",
@ -430,7 +430,7 @@ class PDTSceneProperties(PropertyGroup):
# Was filletrad
fillet_radius: FloatProperty(
name="Fillet Radius", min=0.0, default=1.0, description=PDT_DES_FILLETRAD
name="Fillet Radius", min=0.0, default=1.0, unit="LENGTH", description=PDT_DES_FILLETRAD
)
# Was filletnum
fillet_segments: IntProperty(
@ -454,10 +454,10 @@ class PDTSceneProperties(PropertyGroup):
name="Coordst2", default=(0.0, 0.0, 0.0), subtype="XYZ", description=PDT_DES_TANCEN2
)
tangent_radius0: FloatProperty(
name="Arc Radius 1", min=0.00001, default=1, description=PDT_DES_RADIUS1
name="Arc Radius 1", min=0.00001, default=1, unit="LENGTH", description=PDT_DES_RADIUS1
)
tangent_radius1: FloatProperty(
name="Arc Radius 2", min=0.00001, default=1, description=PDT_DES_RADIUS2
name="Arc Radius 2", min=0.00001, default=1, unit="LENGTH", description=PDT_DES_RADIUS2
)
tangent_point2: FloatVectorProperty(
name="Coordst3", default=(0.0, 0.0, 0.0), subtype="XYZ", description=PDT_DES_TANCEN3
@ -520,6 +520,7 @@ classes = (
pdt_design.PDT_OT_PlacementAbs,
pdt_design.PDT_OT_PlacementDelta,
pdt_design.PDT_OT_PlacementDis,
pdt_design.PDT_OT_PlacementView,
pdt_design.PDT_OT_PlacementCen,
pdt_design.PDT_OT_PlacementPer,
pdt_design.PDT_OT_PlacementNormal,

View File

@ -220,9 +220,9 @@ def command_run(self, context):
mode = command[1].lower()
if (
(operation == "F" and mode not in {"v", "e", "i"})
or (operation in {"D", "E"} and mode not in {"d", "i"})
or (operation in {"D", "E"} and mode not in {"d", "i", "n"}) #new
or (operation == "M" and mode not in {"a", "d", "i", "p", "o", "x", "y", "z"})
or (operation not in {"D", "E", "F", "M"} and mode not in {"a", "d", "i", "p"})
or (operation not in {"D", "E", "F", "M"} and mode not in {"a", "d", "i", "p", "n"}) #new
):
pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
@ -322,15 +322,15 @@ def pdt_help(self, context):
label = self.layout.label
label(text="Primary Letters (Available Secondary Letters):")
label(text="")
label(text="C: Cursor (a, d, i, p)")
label(text="D: Duplicate Geometry (d, i)")
label(text="E: Extrude Geometry (d, i)")
label(text="C: Cursor (a, d, i, p, v)")
label(text="D: Duplicate Geometry (d, i, v)")
label(text="E: Extrude Geometry (d, i, v)")
label(text="F: Fillet (v, e, i)")
label(text="G: Grab (Move) (a, d, i, p)")
label(text="N: New Vertex (a, d, i, p)")
label(text="G: Grab (Move) (a, d, i, p, v)")
label(text="N: New Vertex (a, d, i, p, v)")
label(text="M: Maths Functions (a, d, p, o, x, y, z)")
label(text="P: Pivot Point (a, d, i, p)")
label(text="V: Extrude Vertice Only (a, d, i, p)")
label(text="P: Pivot Point (a, d, i, p, v)")
label(text="V: Extrude Vertice Only (a, d, i, p, v)")
label(text="S: Split Edges (a, d, i, p)")
label(text="?: Quick Help")
label(text="")
@ -341,6 +341,8 @@ def pdt_help(self, context):
label(text="d: Delta (Relative) Coordinates, e.g. 0.5,0,1.2")
label(text="i: Directional (Polar) Coordinates e.g. 2.6,45")
label(text="p: Percent e.g. 67.5")
label(text="n: Work in View Normal Axis")
label(text="")
label(text="- Fillet Options:")
label(text="v: Fillet Vertices")
label(text="e: Fillet Edges")
@ -438,6 +440,19 @@ def command_parse(context):
except ValueError:
values[ind] = "0.0"
ind = ind + 1
if mode == "n":
# View relative mode
if pg.plane == "XZ":
values = [0.0, values[0], 0.0]
elif pg.plane == "YZ":
values = [values[0], 0.0, 0.0]
elif pg.plane == "XY":
values = [0.0, 0.0, values[0]]
else:
if "-" in values[0]:
values = [0.0, 0.0, values[0][1:]]
else:
values = [0.0, 0.0, f"-{values[0]}"]
# Apply System Rounding
decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
values_out = [str(round(float(v), decimal_places)) for v in values]
@ -507,7 +522,7 @@ def move_cursor_pivot(context, pg, operation, mode, obj, verts, values):
"""
# Absolute/Global Coordinates, or Delta/Relative Coordinates
if mode in {"a", "d"}:
if mode in {"a", "d", "n"}:
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
except:
@ -537,8 +552,8 @@ def move_cursor_pivot(context, pg, operation, mode, obj, verts, values):
scene.cursor.location = vector_delta
elif operation == "P":
pg.pivot_loc = vector_delta
elif mode in {"d", "i"}:
if pg.plane == "LO" and mode == "d":
elif mode in {"d", "i", "n"}:
if pg.plane == "LO" and mode in {"d", "n"}:
vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
elif pg.plane == "LO" and mode == "i":
vector_delta = view_dir(pg.distance, pg.angle)
@ -598,7 +613,7 @@ def move_entities(context, pg, operation, mode, obj, bm, verts, values):
except:
raise PDT_InvalidVector
if obj.mode == "EDIT":
for v in verts:
for v in [v for v in bm.verts if v.select]:
v.co = vector_delta - obj_loc
bmesh.ops.remove_doubles(
bm, verts=[v for v in bm.verts if v.select], dist=0.0001
@ -607,8 +622,8 @@ def move_entities(context, pg, operation, mode, obj, bm, verts, values):
for ob in context.view_layer.objects.selected:
ob.location = vector_delta
elif mode in {"d", "i"}:
if mode == "d":
elif mode in {"d", "i", "n"}:
if mode in {"d", "n"}:
# Delta/Relative Coordinates
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
@ -621,7 +636,7 @@ def move_entities(context, pg, operation, mode, obj, bm, verts, values):
except:
raise PDT_InvalidVector
if pg.plane == "LO" and mode == "d":
if pg.plane == "LO" and mode in {"d", "n"}:
vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
elif pg.plane == "LO" and mode == "i":
vector_delta = view_dir(pg.distance, pg.angle)
@ -678,7 +693,7 @@ def add_new_vertex(context, pg, operation, mode, obj, bm, verts, values):
raise PDT_InvalidVector
new_vertex = bm.verts.new(vector_delta - obj_loc)
# Delta/Relative Coordinates
elif mode == "d":
elif mode in {"d", "n"}:
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
except:
@ -852,7 +867,7 @@ def extrude_vertices(context, pg, operation, mode, obj, obj_loc, bm, verts, valu
bm, verts=[v for v in bm.verts if v.select], dist=0.0001
)
# Delta/Relative Coordinates
elif mode == "d":
elif mode in {"d", "n"}:
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
except:
@ -920,7 +935,7 @@ def extrude_geometry(context, pg, operation, mode, obj, bm, values):
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
# Delta/Relative Coordinates
if mode == "d":
if mode in {"d", "n"}:
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
except:
@ -947,7 +962,7 @@ def extrude_geometry(context, pg, operation, mode, obj, bm, values):
faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)]
del ret
if pg.plane == "LO" and mode == "d":
if pg.plane == "LO" and mode in {"d", "n"}:
vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
elif pg.plane == "LO" and mode == "i":
vector_delta = view_dir(pg.distance, pg.angle)
@ -979,7 +994,7 @@ def duplicate_geometry(context, pg, operation, mode, obj, bm, values):
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
# Delta/Relative Coordinates
if mode == "d":
if mode in {"d", "n"}:
try:
vector_delta = vector_build(context, pg, obj, operation, values, 3)
except:
@ -1006,7 +1021,7 @@ def duplicate_geometry(context, pg, operation, mode, obj, bm, values):
faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)]
del ret
if pg.plane == "LO" and mode == "d":
if pg.plane == "LO" and mode in {"d", "n"}:
vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
elif pg.plane == "LO" and mode == "i":
vector_delta = view_dir(pg.distance, pg.angle)

View File

@ -184,7 +184,7 @@ class PDT_OT_PlacementDelta(Operator):
f",{str(round(pg.cartesian_coords.z, decimal_places))}"
)
elif operation == "EV":
# Extrue Vertices
# Extrude Vertices
pg.command = (
f"vd{str(round(pg.cartesian_coords.x, decimal_places))}"
f",{str(round(pg.cartesian_coords.y, decimal_places))}"
@ -198,7 +198,7 @@ class PDT_OT_PlacementDelta(Operator):
f",{str(round(pg.cartesian_coords.z, decimal_places))}"
)
elif operation == "EG":
# Extrue Geometry
# Extrude Geometry
pg.command = (
f"ed{str(round(pg.cartesian_coords.x, decimal_places))}"
f",{str(round(pg.cartesian_coords.y, decimal_places))}"
@ -299,6 +299,82 @@ class PDT_OT_PlacementDis(Operator):
return {"FINISHED"}
class PDT_OT_PlacementView(Operator):
"""Use Distance Input for View Normal Axis Operations"""
bl_idname = "pdt.view_axis"
bl_label = "View Normal Axis Mode"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
"""Manipulates Geometry, or Objects by View Normal Axis Offset (Increment).
Note:
- Reads pg.operation from Operation Mode Selector as 'operation'
- Reads pg.select, pg.plane, pg.cartesian_coords scene variables to:
-- set position of CUrsor (CU)
-- set position of Pivot Point (PP)
-- MoVe geometry/objects (MV)
-- Extrude Vertices (EV)
-- Split Edges (SE)
-- add a New Vertex (NV)
-- Duplicate Geometry (DG)
-- Extrude Geometry (EG)
Invalid Options result in self.report Error.
Args:
context: Blender bpy.context instance.
Returns:
Status Set.
"""
pg = context.scene.pdt_pg
operation = pg.operation
decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
if operation == "CU":
# Cursor
pg.command = (
f"cn{str(round(pg.distance, decimal_places))}"
)
elif operation == "PP":
# Pivot Point
pg.command = (
f"pn{str(round(pg.distance, decimal_places))}"
)
elif operation == "MV":
# Move Entities
pg.command = (
f"gn{str(round(pg.distance, decimal_places))}"
)
elif operation == "NV":
# New Vertex
pg.command = (
f"nn{str(round(pg.distance, decimal_places))}"
)
elif operation == "EV":
# Extrude Vertices
pg.command = (
f"vn{str(round(pg.distance, decimal_places))}"
)
elif operation == "DG":
# Duplicate Entities
pg.command = (
f"dn{str(round(pg.distance, decimal_places))}"
)
elif operation == "EG":
# Extrude Geometry
pg.command = (
f"en{str(round(pg.distance, decimal_places))}"
)
else:
error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_DEL}"
self.report({"ERROR"}, error_message)
return {"FINISHED"}
class PDT_OT_PlacementPer(Operator):
"""Use Percentage Placement"""
@ -703,3 +779,9 @@ class PDT_OT_Taper(Operator):
pg = context.scene.pdt_pg
pg.command = f"tap"
return {"FINISHED"}
#class PDT_Extrude_Modal(Operator):
# """Extrude Modal Plane Along Normal Axis"""
# bl_idname = "pdt.extrude_modal"
# bl_label = "Extrude Modal Normal"
# bl_options = {"REGISTER", "UNDO"}

View File

@ -62,7 +62,8 @@ from .pdt_msg_strings import (
PDT_LAB_TAPERAXES,
PDT_LAB_TOOLS,
PDT_LAB_USEVERTS,
PDT_LAB_VARIABLES
PDT_LAB_VARIABLES,
PDT_LAB_VIEW
)
def ui_width():
@ -148,6 +149,8 @@ class PDT_PT_PanelDesign(Panel):
row.prop(pdt_pg, "angle", text=PDT_LAB_ANGLEVALUE)
row = box.row()
row.operator("pdt.distance", icon="EMPTY_AXIS", text=f"{PDT_LAB_DIR} »")
row.operator("pdt.view_axis", icon="EMPTY_AXIS", text=f"{PDT_LAB_VIEW} »")
row = box.row()
row.prop(pdt_pg, "flip_angle", text=PDT_LAB_FLIPANGLE)
# ---------------------
@ -437,7 +440,7 @@ class PDT_PT_PanelTangent(Panel):
row.label(text=f"Working {PDT_LAB_PLANE}:")
row.prop(pdt_pg, "plane", text="")
row = layout.row()
row.label(text="Tan Mode")
row.label(text="Tangent Mode")
row.prop(pdt_pg, "tangent_mode", text="")
row = layout.row()
row.operator("pdt.tangentoperatesel", text="Tangents from Selection", icon="NONE")
@ -448,7 +451,7 @@ class PDT_PT_PanelTangent(Panel):
box = layout.box()
row = box.row()
split = row.split(factor=0.35, align=True)
split.label(text="Tan Point")
split.label(text="Tangent Point")
split.prop(pdt_pg, "tangent_point2", text="")
row = box.row()
row.operator("pdt.tangentset3", text="from Cursor")

View File

@ -80,6 +80,7 @@ PDT_LAB_PIVOTWIDTH = "" # Intentionally left blank
PDT_LAB_PIVOTALPHA = "" # Intentionally left blank
PDT_LAB_PIVOTLOC = "" # Intentionally left blank
PDT_LAB_PIVOTLOCH = "Location"
PDT_LAB_VIEW = "View Normal Axis"
#
# Error Message
#