583 lines
20 KiB
Python
583 lines
20 KiB
Python
# -*- coding:utf-8 -*-
|
|
|
|
# ##### 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 #####
|
|
|
|
# <pep8 compliant>
|
|
|
|
# ----------------------------------------------------------
|
|
# Author: Stephen Leger (s-leger)
|
|
#
|
|
# ----------------------------------------------------------
|
|
import bpy
|
|
import os
|
|
import subprocess
|
|
from bl_operators.presets import AddPresetBase
|
|
from mathutils import Vector
|
|
from bpy.props import StringProperty
|
|
from .archipack_gl import (
|
|
ThumbHandle, Screen, GlRect,
|
|
GlPolyline, GlPolygon, GlText, GlHandle
|
|
)
|
|
preset_paths = bpy.utils.script_paths("presets")
|
|
addons_paths = bpy.utils.script_paths("addons")
|
|
|
|
|
|
class CruxHandle(GlHandle):
|
|
|
|
def __init__(self, sensor_size, depth):
|
|
GlHandle.__init__(self, sensor_size, 0, True, False)
|
|
self.branch_0 = GlPolygon((1, 1, 1, 1), d=2)
|
|
self.branch_1 = GlPolygon((1, 1, 1, 1), d=2)
|
|
self.branch_2 = GlPolygon((1, 1, 1, 1), d=2)
|
|
self.branch_3 = GlPolygon((1, 1, 1, 1), d=2)
|
|
self.depth = depth
|
|
|
|
def set_pos(self, pos_2d):
|
|
self.pos_2d = pos_2d
|
|
o = pos_2d
|
|
w = 0.5 * self.sensor_width
|
|
d = self.depth
|
|
c = d / 1.4242
|
|
s = w - c
|
|
p0 = o + Vector((s, w))
|
|
p1 = o + Vector((w, s))
|
|
p2 = o + Vector((c, 0))
|
|
p3 = o + Vector((w, -s))
|
|
p4 = o + Vector((s, -w))
|
|
p5 = o + Vector((0, -c))
|
|
p6 = o + Vector((-s, -w))
|
|
p7 = o + Vector((-w, -s))
|
|
p8 = o + Vector((-c, 0))
|
|
p9 = o + Vector((-w, s))
|
|
p10 = o + Vector((-s, w))
|
|
p11 = o + Vector((0, c))
|
|
self.branch_0.set_pos([p11, p0, p1, p2, o])
|
|
self.branch_1.set_pos([p2, p3, p4, p5, o])
|
|
self.branch_2.set_pos([p5, p6, p7, p8, o])
|
|
self.branch_3.set_pos([p8, p9, p10, p11, o])
|
|
|
|
@property
|
|
def pts(self):
|
|
return [self.pos_2d]
|
|
|
|
@property
|
|
def sensor_center(self):
|
|
return self.pos_2d
|
|
|
|
def draw(self, context, render=False):
|
|
self.render = render
|
|
self.branch_0.colour_inactive = self.colour
|
|
self.branch_1.colour_inactive = self.colour
|
|
self.branch_2.colour_inactive = self.colour
|
|
self.branch_3.colour_inactive = self.colour
|
|
self.branch_0.draw(context)
|
|
self.branch_1.draw(context)
|
|
self.branch_2.draw(context)
|
|
self.branch_3.draw(context)
|
|
|
|
|
|
class SeekBox(GlText, GlHandle):
|
|
"""
|
|
Text input to filter items by label
|
|
TODO:
|
|
- add cross to empty text
|
|
- get text from keyboard
|
|
"""
|
|
|
|
def __init__(self):
|
|
GlHandle.__init__(self, 0, 0, True, False, d=2)
|
|
GlText.__init__(self, d=2)
|
|
self.sensor_width = 250
|
|
self.pos_3d = Vector((0, 0))
|
|
self.bg = GlRect(colour=(0, 0, 0, 0.7))
|
|
self.frame = GlPolyline((1, 1, 1, 1), d=2)
|
|
self.frame.closed = True
|
|
self.cancel = CruxHandle(16, 4)
|
|
self.line_pos = 0
|
|
|
|
@property
|
|
def pts(self):
|
|
return [self.pos_3d]
|
|
|
|
def set_pos(self, context, pos_2d):
|
|
x, ty = self.text_size(context)
|
|
w = self.sensor_width
|
|
y = 12
|
|
pos_2d.y += y
|
|
pos_2d.x -= 0.5 * w
|
|
self.pos_2d = pos_2d.copy()
|
|
self.pos_3d = pos_2d.copy()
|
|
self.pos_3d.x += 6
|
|
self.sensor_height = y
|
|
p0 = pos_2d + Vector((w, -0.5 * y))
|
|
p1 = pos_2d + Vector((w, 1.5 * y))
|
|
p2 = pos_2d + Vector((0, 1.5 * y))
|
|
p3 = pos_2d + Vector((0, -0.5 * y))
|
|
self.bg.set_pos([p0, p2])
|
|
self.frame.set_pos([p0, p1, p2, p3])
|
|
self.cancel.set_pos(pos_2d + Vector((w + 15, 0.5 * y)))
|
|
|
|
def keyboard_entry(self, context, event):
|
|
c = event.ascii
|
|
if c:
|
|
if c == ",":
|
|
c = "."
|
|
self.label = self.label[:self.line_pos] + c + self.label[self.line_pos:]
|
|
self.line_pos += 1
|
|
|
|
if self.label:
|
|
if event.type == 'BACK_SPACE':
|
|
self.label = self.label[:self.line_pos - 1] + self.label[self.line_pos:]
|
|
self.line_pos -= 1
|
|
|
|
elif event.type == 'DEL':
|
|
self.label = self.label[:self.line_pos] + self.label[self.line_pos + 1:]
|
|
|
|
elif event.type == 'LEFT_ARROW':
|
|
self.line_pos = (self.line_pos - 1) % (len(self.label) + 1)
|
|
|
|
elif event.type == 'RIGHT_ARROW':
|
|
self.line_pos = (self.line_pos + 1) % (len(self.label) + 1)
|
|
|
|
def draw(self, context):
|
|
self.bg.draw(context)
|
|
self.frame.draw(context)
|
|
GlText.draw(self, context)
|
|
self.cancel.draw(context)
|
|
|
|
@property
|
|
def sensor_center(self):
|
|
return self.pos_3d
|
|
|
|
|
|
class PresetMenuItem():
|
|
def __init__(self, thumbsize, preset, image=None):
|
|
name = bpy.path.display_name_from_filepath(preset)
|
|
self.preset = preset
|
|
self.image = image
|
|
self.image.gl_load()
|
|
self.handle = ThumbHandle(thumbsize, name, self.image, draggable=True)
|
|
self.enable = True
|
|
|
|
def filter(self, keywords):
|
|
for key in keywords:
|
|
if key not in self.handle.label.label:
|
|
return False
|
|
return True
|
|
|
|
def cleanup(self):
|
|
if self.image:
|
|
self.image.gl_free()
|
|
# bpy.data.images.remove(self.image)
|
|
|
|
def set_pos(self, context, pos):
|
|
self.handle.set_pos(context, pos)
|
|
|
|
def check_hover(self, mouse_pos):
|
|
self.handle.check_hover(mouse_pos)
|
|
|
|
def mouse_press(self):
|
|
if self.handle.hover:
|
|
self.handle.hover = False
|
|
self.handle.active = True
|
|
return True
|
|
return False
|
|
|
|
def draw(self, context):
|
|
if self.enable:
|
|
self.handle.draw(context)
|
|
|
|
|
|
class PresetMenu():
|
|
|
|
keyboard_type = {
|
|
'BACK_SPACE', 'DEL',
|
|
'LEFT_ARROW', 'RIGHT_ARROW'
|
|
}
|
|
|
|
def __init__(self, context, category, thumbsize=Vector((150, 100))):
|
|
self.imageList = []
|
|
self.menuItems = []
|
|
self.thumbsize = thumbsize
|
|
file_list = self.scan_files(category)
|
|
self.default_image = None
|
|
self.load_default_image()
|
|
for filepath in file_list:
|
|
self.make_menuitem(filepath)
|
|
self.margin = 50
|
|
self.y_scroll = 0
|
|
self.scroll_max = 1000
|
|
self.spacing = Vector((25, 25))
|
|
self.screen = Screen(self.margin)
|
|
self.mouse_pos = Vector((0, 0))
|
|
self.bg = GlRect(colour=(0, 0, 0, 0.7))
|
|
self.border = GlPolyline((0.7, 0.7, 0.7, 1), d=2)
|
|
self.keywords = SeekBox()
|
|
self.keywords.colour_normal = (1, 1, 1, 1)
|
|
self.border.closed = True
|
|
self.set_pos(context)
|
|
|
|
def load_default_image(self):
|
|
img_idx = bpy.data.images.find("missing.png")
|
|
if img_idx > -1:
|
|
self.default_image = bpy.data.images[img_idx]
|
|
self.imageList.append(self.default_image.filepath_raw)
|
|
return
|
|
dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
sub_path = "presets" + os.path.sep + "missing.png"
|
|
filepath = os.path.join(dir_path, sub_path)
|
|
if os.path.exists(filepath) and os.path.isfile(filepath):
|
|
self.default_image = bpy.data.images.load(filepath=filepath)
|
|
self.imageList.append(self.default_image.filepath_raw)
|
|
if self.default_image is None:
|
|
raise EnvironmentError("archipack/presets/missing.png not found")
|
|
|
|
def scan_files(self, category):
|
|
file_list = []
|
|
"""
|
|
# load default presets
|
|
dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
sub_path = "presets" + os.path.sep + category
|
|
presets_path = os.path.join(dir_path, sub_path)
|
|
if os.path.exists(presets_path):
|
|
file_list += [presets_path + os.path.sep + f[:-3]
|
|
for f in os.listdir(presets_path)
|
|
if f.endswith('.py') and
|
|
not f.startswith('.')]
|
|
"""
|
|
# load user def presets
|
|
for path in preset_paths:
|
|
presets_path = os.path.join(path, category)
|
|
if os.path.exists(presets_path):
|
|
file_list += [presets_path + os.path.sep + f[:-3]
|
|
for f in os.listdir(presets_path)
|
|
if f.endswith('.py') and
|
|
not f.startswith('.')]
|
|
|
|
file_list.sort()
|
|
return file_list
|
|
|
|
def clearImages(self):
|
|
for item in self.menuItems:
|
|
item.cleanup()
|
|
for image in bpy.data.images:
|
|
if image.filepath_raw in self.imageList:
|
|
# image.user_clear()
|
|
bpy.data.images.remove(image, do_unlink=True)
|
|
self.imageList.clear()
|
|
|
|
|
|
def make_menuitem(self, filepath):
|
|
"""
|
|
@TODO:
|
|
Lazy load images
|
|
"""
|
|
image = None
|
|
img_idx = bpy.data.images.find(os.path.basename(filepath) + '.png')
|
|
if img_idx > -1 and bpy.data.images[img_idx].filepath_raw == filepath:
|
|
image = bpy.data.images[img_idx]
|
|
self.imageList.append(image.filepath_raw)
|
|
elif os.path.exists(filepath + '.png') and os.path.isfile(filepath + '.png'):
|
|
image = bpy.data.images.load(filepath=filepath + '.png')
|
|
if hasattr(image, "colorspace_settings"):
|
|
image.colorspace_settings.name = 'Raw'
|
|
self.imageList.append(image)
|
|
if image is None:
|
|
image = self.default_image
|
|
item = PresetMenuItem(self.thumbsize, filepath + '.py', image)
|
|
self.menuItems.append(item)
|
|
|
|
def set_pos(self, context):
|
|
|
|
x_min, x_max, y_min, y_max = self.screen.size(context)
|
|
y_max -= 20
|
|
p0, p1, p2, p3 = Vector((x_min, y_min)), Vector((x_min, y_max)), Vector((x_max, y_max)), Vector((x_max, y_min))
|
|
self.bg.set_pos([p0, p2])
|
|
self.border.set_pos([p0, p1, p2, p3])
|
|
x_min += 0.5 * self.thumbsize.x + 0.5 * self.margin
|
|
x_max -= 0.5 * self.thumbsize.x + 0.5 * self.margin
|
|
y_max -= 0.5 * self.thumbsize.y + 0.5 * self.margin
|
|
y_min += 0.5 * self.margin
|
|
x = x_min
|
|
y = y_max + self.y_scroll
|
|
n_rows = 0
|
|
|
|
self.keywords.set_pos(context, p1 + 0.5 * (p2 - p1))
|
|
keywords = self.keywords.label.split(" ")
|
|
|
|
for item in self.menuItems:
|
|
if y > y_max or y < y_min:
|
|
item.enable = False
|
|
else:
|
|
item.enable = True
|
|
|
|
# filter items by name
|
|
if len(keywords) > 0 and not item.filter(keywords):
|
|
item.enable = False
|
|
continue
|
|
|
|
item.set_pos(context, Vector((x, y)))
|
|
x += self.thumbsize.x + self.spacing.x
|
|
if x > x_max:
|
|
n_rows += 1
|
|
x = x_min
|
|
y -= self.thumbsize.y + self.spacing.y
|
|
|
|
self.scroll_max = max(0, n_rows - 1) * (self.thumbsize.y + self.spacing.y)
|
|
|
|
def draw(self, context):
|
|
self.bg.draw(context)
|
|
self.border.draw(context)
|
|
self.keywords.draw(context)
|
|
for item in self.menuItems:
|
|
item.draw(context)
|
|
|
|
def mouse_press(self, context, event):
|
|
self.mouse_position(event)
|
|
|
|
if self.keywords.cancel.hover:
|
|
self.keywords.label = ""
|
|
self.keywords.line_pos = 0
|
|
self.set_pos(context)
|
|
|
|
for item in self.menuItems:
|
|
if item.enable and item.mouse_press():
|
|
# load item preset
|
|
return item.preset
|
|
return None
|
|
|
|
def mouse_position(self, event):
|
|
self.mouse_pos.x, self.mouse_pos.y = event.mouse_region_x, event.mouse_region_y
|
|
|
|
def mouse_move(self, context, event):
|
|
self.mouse_position(event)
|
|
self.keywords.check_hover(self.mouse_pos)
|
|
self.keywords.cancel.check_hover(self.mouse_pos)
|
|
for item in self.menuItems:
|
|
item.check_hover(self.mouse_pos)
|
|
|
|
def scroll_up(self, context, event):
|
|
self.y_scroll = max(0, self.y_scroll - (self.thumbsize.y + self.spacing.y))
|
|
self.set_pos(context)
|
|
# print("scroll_up %s" % (self.y_scroll))
|
|
|
|
def scroll_down(self, context, event):
|
|
self.y_scroll = min(self.scroll_max, self.y_scroll + (self.thumbsize.y + self.spacing.y))
|
|
self.set_pos(context)
|
|
# print("scroll_down %s" % (self.y_scroll))
|
|
|
|
def keyboard_entry(self, context, event):
|
|
self.keywords.keyboard_entry(context, event)
|
|
self.set_pos(context)
|
|
|
|
|
|
class PresetMenuOperator():
|
|
|
|
preset_operator : StringProperty(
|
|
options={'SKIP_SAVE'},
|
|
default="script.execute_preset"
|
|
)
|
|
|
|
def __init__(self):
|
|
self.menu = None
|
|
self._handle = None
|
|
|
|
def exit(self, context):
|
|
self.menu.clearImages()
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
|
|
|
def draw_handler(self, _self, context):
|
|
self.menu.draw(context)
|
|
|
|
def modal(self, context, event):
|
|
if self.menu is None:
|
|
return {'FINISHED'}
|
|
context.area.tag_redraw()
|
|
if event.type == 'MOUSEMOVE':
|
|
self.menu.mouse_move(context, event)
|
|
elif event.type == 'WHEELUPMOUSE' or \
|
|
(event.type == 'UP_ARROW' and event.value == 'PRESS'):
|
|
self.menu.scroll_up(context, event)
|
|
elif event.type == 'WHEELDOWNMOUSE' or \
|
|
(event.type == 'DOWN_ARROW' and event.value == 'PRESS'):
|
|
self.menu.scroll_down(context, event)
|
|
elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
|
preset = self.menu.mouse_press(context, event)
|
|
if preset is not None:
|
|
self.exit(context)
|
|
po = self.preset_operator.split(".")
|
|
op = getattr(getattr(bpy.ops, po[0]), po[1])
|
|
if self.preset_operator == 'script.execute_preset':
|
|
# call from preset menu
|
|
# ensure right active_object class
|
|
o = context.active_object
|
|
if o.data and self.preset_subdir in o.data:
|
|
d = getattr(o.data, self.preset_subdir)[0]
|
|
elif self.preset_subdir in o:
|
|
d = getattr(o, self.preset_subdir)[0]
|
|
if d is not None:
|
|
d.auto_update = False
|
|
# print("Archipack execute_preset loading auto_update:%s" % d.auto_update)
|
|
op('INVOKE_DEFAULT', filepath=preset, menu_idname=self.bl_idname)
|
|
# print("Archipack execute_preset loaded auto_update: %s" % d.auto_update)
|
|
d.auto_update = True
|
|
else:
|
|
# call draw operator
|
|
if op.poll():
|
|
op('INVOKE_DEFAULT', filepath=preset)
|
|
else:
|
|
print("Poll failed")
|
|
return {'FINISHED'}
|
|
elif event.ascii or (
|
|
event.type in self.menu.keyboard_type and
|
|
event.value == 'RELEASE'):
|
|
self.menu.keyboard_entry(context, event)
|
|
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
|
self.exit(context)
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
if context.area.type == 'VIEW_3D':
|
|
|
|
# with alt pressed on invoke, will bypass menu operator and
|
|
# call preset_operator
|
|
# allow start drawing linked copy of active object
|
|
if event.alt or event.ctrl:
|
|
po = self.preset_operator.split(".")
|
|
op = getattr(getattr(bpy.ops, po[0]), po[1])
|
|
d = context.active_object.data
|
|
|
|
if d is not None and self.preset_subdir in d and op.poll():
|
|
op('INVOKE_DEFAULT')
|
|
else:
|
|
self.report({'WARNING'}, "Active object must be a " + self.preset_subdir.split("_")[1].capitalize())
|
|
return {'CANCELLED'}
|
|
return {'FINISHED'}
|
|
|
|
self.menu = PresetMenu(context, self.preset_subdir)
|
|
|
|
# the arguments we pass the the callback
|
|
args = (self, context)
|
|
# Add the region OpenGL drawing callback
|
|
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
|
|
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_handler, args, 'WINDOW', 'POST_PIXEL')
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
else:
|
|
self.report({'WARNING'}, "View3D not found, cannot show preset flinger")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
class ArchipackPreset(AddPresetBase):
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
o = context.active_object
|
|
return o is not None and \
|
|
o.data is not None and \
|
|
"archipack_" + cls.__name__[13:-7] in o.data
|
|
|
|
@property
|
|
def preset_subdir(self):
|
|
return "archipack_" + self.__class__.__name__[13:-7]
|
|
|
|
@property
|
|
def blacklist(self):
|
|
"""
|
|
properties black list for presets
|
|
may override on addon basis
|
|
"""
|
|
return []
|
|
|
|
@property
|
|
def preset_values(self):
|
|
blacklist = self.blacklist
|
|
blacklist.extend(bpy.types.Mesh.bl_rna.properties.keys())
|
|
d = getattr(bpy.context.active_object.data, self.preset_subdir)[0]
|
|
props = d.rna_type.bl_rna.properties.items()
|
|
ret = []
|
|
for prop_id, prop in props:
|
|
if prop_id not in blacklist:
|
|
if not (prop.is_hidden or prop.is_skip_save):
|
|
ret.append("d.%s" % prop_id)
|
|
ret.sort()
|
|
return ret
|
|
|
|
@property
|
|
def preset_defines(self):
|
|
o = bpy.context.active_object
|
|
m = o.archipack_material[0]
|
|
return [
|
|
"d = bpy.context.active_object.data." + self.preset_subdir + "[0]",
|
|
"bpy.ops.archipack.material(category='" + m.category + "', material='" + m.material + "')"
|
|
]
|
|
|
|
def pre_cb(self, context):
|
|
return
|
|
|
|
def remove(self, context, filepath):
|
|
# remove preset
|
|
os.remove(filepath)
|
|
# remove thumb
|
|
os.remove(filepath[:-3] + ".png")
|
|
|
|
def background_render(self, context, cls, preset):
|
|
generator = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + "archipack_thumbs.py"
|
|
addon_name = __name__.split('.')[0]
|
|
matlib_path = context.preferences.addons[addon_name].preferences.matlib_path
|
|
# Run external instance of blender like the original thumbnail generator.
|
|
cmd = [
|
|
bpy.app.binary_path,
|
|
"--background",
|
|
"--factory-startup",
|
|
"-noaudio",
|
|
# "--addons", addon_name,
|
|
"--python", generator,
|
|
"--",
|
|
"addon:" + addon_name,
|
|
"matlib:" + matlib_path,
|
|
"cls:" + cls,
|
|
"preset:" + preset
|
|
]
|
|
|
|
subprocess.Popen(cmd)
|
|
|
|
def post_cb(self, context):
|
|
|
|
if not self.remove_active:
|
|
|
|
name = self.name.strip()
|
|
if not name:
|
|
return
|
|
|
|
filename = self.as_filename(name)
|
|
target_path = os.path.join("presets", self.preset_subdir)
|
|
target_path = bpy.utils.user_resource('SCRIPTS',
|
|
target_path,
|
|
create=True)
|
|
|
|
preset = os.path.join(target_path, filename) + ".py"
|
|
cls = self.preset_subdir[10:]
|
|
# print("post cb cls:%s preset:%s" % (cls, preset))
|
|
self.background_render(context, cls, preset)
|
|
|
|
return
|