0
$\begingroup$

I'd like to create a custom user interface for assigning textures/images to a custom object type. So far I managed to create a file selector. However, I can't seem to find a way to create and update a preview window when another object is selected. As far as I know the preview should be rendered using the layout.template_preview() method. This works sometimes, but usually Blender would crash when I try to use it. (I'm not quite sure how it should be used properly.)

Below is a screenshot of what I have so far. I have left out the preview window because it didn't really work.

enter image description here

The code that I have:

class MM2CITY_PT_facade_storey(ObjectButtonsPanel, Panel):
    bl_label = "Storey"
    bl_parent_id = "MM2CITY_PT_main"

    @classmethod
    def poll(cls, context):
        ob = context.object
        return ob.mm2city.generator_type == 'FACADE' and ob.type == 'CURVE'

    def draw(self, context):
        layout = self.layout
        ob = context.object

        if ob.mm2city.generator_type == 'FACADE' and len(ob.mm2city_facade.storeys) > 0:
            storey = ob.mm2city_facade.storeys[context.scene.mm2city_active_storey_index]

            col = layout.column()
            row = col.row(align=True)

            if storey.is_sliver:
                row.prop(storey, "absolute_height", text="Height (Absolute)")
            else:
                row.prop(storey, "relative_height", text="Height (Relative)")

            layout.template_ID(storey, "select_image", new="image.new", open="image.open")

            tex = context.texture
            slot = getattr(context, "texture_slot", None)

            # This doesn't quite work:
            layout.template_preview(tex, slot=slot)

Thanks in advance!

Update:

I've been able to create a preview window by using the layout.template_ID_preview(). However, the preview itself has the same input like layout.template_ID() to change the texture name. Is there any way to remove this input from the preview? See screenshot:

enter image description here

Updated code:

class MM2CITY_PT_facade_storey(ObjectButtonsPanel, Panel):
    bl_label = "Storey"
    bl_parent_id = "MM2CITY_PT_main"

    @classmethod
    def poll(cls, context):
        ob = context.object
        return ob.mm2city.generator_type == 'FACADE' and ob.type == 'CURVE'

    def draw(self, context):
        layout = self.layout

        ob = context.object

        if ob.mm2city.generator_type == 'FACADE' and len(ob.mm2city_facade.storeys) > 0:
            storey = ob.mm2city_facade.storeys[context.scene.mm2city_active_storey_index]

            col = layout.column()
            row = col.row(align=True)

            if storey.is_sliver:
                row.prop(storey, "absolute_height", text="Height (Absolute)")
                row = col.row(align=True)
                row.prop(storey, "texture_scale", text="Texture Scale")
            else:
                row.prop(storey, "relative_height", text="Height (Relative)")
                row = col.row()
                row.prop(storey, "u_repeat", text="Texture U Repeat")
                row.prop(storey, "v_repeat", text="Texture V Repeat")

            layout.template_ID(storey, "texture", new="image.new", open="image.open")

            tex = storey.texture

            if tex is not None:
                layout.template_ID_preview(storey, property="texture", hide_buttons=True)
$\endgroup$

1 Answer 1

0
$\begingroup$

This is what I did in the end; converting the Image to a Texture on the fly, and using template_preview for displaying the image and template_ID for the controls below it.

Result:

Blender panel UI

Panel code:

class MM2CITY_PT_facade_storey(ObjectButtonsPanel, Panel):
    bl_label = "Storey"
    bl_parent_id = "MM2CITY_PT_main"

    @classmethod
    def poll(cls, context):
        ob = context.object
        return ob.mm2city.generator_type == 'FACADE' and ob.type == 'CURVE'

    def draw(self, context):
        layout = self.layout
        ob = context.object

        if ob.mm2city.generator_type == 'FACADE' and len(ob.mm2city_facade.storeys) > 0:
            storey = ob.mm2city_facade.storeys[ob.mm2city_facade.active_storey_index]

            # - Other UI code omitted for brevity (see original question) -

            layout.label(text="Texture:")

            id = bpy.data.textures.find('.hidden')
            if id == -1:
                return
            texture = bpy.data.textures['.hidden']
            
            row = layout.row(align=True)
            row.template_preview(texture)

            layout.template_ID(storey, "texture", new="image.new", open="image.open")

My property class:

class MM2CITYStoreyProperties(bpy.types.PropertyGroup):
    texture: bpy.props.PointerProperty(
        name='Image',
        type=bpy.types.Image,
        update=storey_texture_change)

Whenever texture is changed (which is in fact an Image), this update function is called:

def storey_texture_change(self, context):
    ob = context.active_object
    storey = ob.mm2city_facade.storeys[ob.mm2city_facade.active_storey_index]
    img = storey.texture

    update_image_preview(img)

Which in turn calls this function:

def update_image_preview(image):
    id = bpy.data.textures.find('.hidden')
    if id == -1:
        texture = bpy.data.textures.new(name='.hidden', type="IMAGE")
    else:
        texture = bpy.data.textures['.hidden']
    texture.image = image
    texture.extension = 'REPEAT' # Can be: EXTEND, CLIP, CLIP_CUBE, REPEAT or CHECKER

As you can see, the texture name is prefixed with a dot. This makes the texture hidden to the rest of the blend-file.

I also call the above function from any other handlers where I wish to update the texture, for example when the selected item in the list is changed:

def change_active_storey(self, context):
    ob = context.active_object
    storey = ob.mm2city_facade.storeys[ob.mm2city_facade.active_storey_index]
    img = storey.texture

    update_image_preview(img)

... or when the selected object is changed:

previous_object = None

def msgbus_callback(*args):
    global previous_object

    ob = bpy.context.active_object

    if ob.mm2city.generator_type != 'FACADE' or ob.type != 'CURVE':
        return

    if previous_object is None:
        previous_object = ob
    elif previous_object == ob:
        return

    storey = ob.mm2city_facade.storeys[ob.mm2city_facade.active_storey_index]
    img = storey.texture

    previous_object = ob

    update_image_preview(img)

    
def subscribe_to_obj():
    bpy.msgbus.subscribe_rna(
        key=(bpy.types.LayerObjects, "active"),
        owner=object(),
        args=tuple(),
        notify=msgbus_callback,
    )

subscribe_to_obj() is called from the add-on's register() function.

I hope this will help someone facing the same issue!

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .