Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
# Example of a custom widget that defines its own geometry.
#
# Usage: Select a light in the 3D view and drag the arrow at it's rear
# to change it's energy value.
#
import bpy
from bpy.types import (
Gizmo,
GizmoGroup,
)
# Coordinates (each one is a triangle).
custom_shape_verts = (
(3.0, 1.0, -1.0), (2.0, 2.0, -1.0), (3.0, 3.0, -1.0),
(1.0, 3.0, 1.0), (3.0, 3.0, -1.0), (1.0, 3.0, -1.0),
(3.0, 3.0, 1.0), (3.0, 1.0, -1.0), (3.0, 3.0, -1.0),
(2.0, 0.0, 1.0), (3.0, 1.0, -1.0), (3.0, 1.0, 1.0),
(2.0, 0.0, -1.0), (2.0, 2.0, 1.0), (2.0, 2.0, -1.0),
(2.0, 2.0, -1.0), (0.0, 2.0, 1.0), (0.0, 2.0, -1.0),
(1.0, 3.0, 1.0), (2.0, 2.0, 1.0), (3.0, 3.0, 1.0),
(0.0, 2.0, -1.0), (1.0, 3.0, 1.0), (1.0, 3.0, -1.0),
(2.0, 2.0, 1.0), (3.0, 1.0, 1.0), (3.0, 3.0, 1.0),
(2.0, 2.0, -1.0), (1.0, 3.0, -1.0), (3.0, 3.0, -1.0),
(-3.0, -1.0, -1.0), (-2.0, -2.0, -1.0), (-3.0, -3.0, -1.0),
(-1.0, -3.0, 1.0), (-3.0, -3.0, -1.0), (-1.0, -3.0, -1.0),
(-3.0, -3.0, 1.0), (-3.0, -1.0, -1.0), (-3.0, -3.0, -1.0),
(-2.0, 0.0, 1.0), (-3.0, -1.0, -1.0), (-3.0, -1.0, 1.0),
(-2.0, 0.0, -1.0), (-2.0, -2.0, 1.0), (-2.0, -2.0, -1.0),
(-2.0, -2.0, -1.0), (0.0, -2.0, 1.0), (0.0, -2.0, -1.0),
(-1.0, -3.0, 1.0), (-2.0, -2.0, 1.0), (-3.0, -3.0, 1.0),
(0.0, -2.0, -1.0), (-1.0, -3.0, 1.0), (-1.0, -3.0, -1.0),
(-2.0, -2.0, 1.0), (-3.0, -1.0, 1.0), (-3.0, -3.0, 1.0),
(-2.0, -2.0, -1.0), (-1.0, -3.0, -1.0), (-3.0, -3.0, -1.0),
(1.0, -1.0, 0.0), (-1.0, -1.0, 0.0), (0.0, 0.0, -5.0),
(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (0.0, 0.0, 5.0),
(1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 0.0, 5.0),
(1.0, 1.0, 0.0), (-1.0, 1.0, 0.0), (0.0, 0.0, 5.0),
(-1.0, 1.0, 0.0), (-1.0, -1.0, 0.0), (0.0, 0.0, 5.0),
(-1.0, -1.0, 0.0), (-1.0, 1.0, 0.0), (0.0, 0.0, -5.0),
(-1.0, 1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 0.0, -5.0),
(1.0, 1.0, 0.0), (1.0, -1.0, 0.0), (0.0, 0.0, -5.0),
(3.0, 1.0, -1.0), (2.0, 0.0, -1.0), (2.0, 2.0, -1.0),
(1.0, 3.0, 1.0), (3.0, 3.0, 1.0), (3.0, 3.0, -1.0),
(3.0, 3.0, 1.0), (3.0, 1.0, 1.0), (3.0, 1.0, -1.0),
(2.0, 0.0, 1.0), (2.0, 0.0, -1.0), (3.0, 1.0, -1.0),
(2.0, 0.0, -1.0), (2.0, 0.0, 1.0), (2.0, 2.0, 1.0),
(2.0, 2.0, -1.0), (2.0, 2.0, 1.0), (0.0, 2.0, 1.0),
(1.0, 3.0, 1.0), (0.0, 2.0, 1.0), (2.0, 2.0, 1.0),
(0.0, 2.0, -1.0), (0.0, 2.0, 1.0), (1.0, 3.0, 1.0),
(2.0, 2.0, 1.0), (2.0, 0.0, 1.0), (3.0, 1.0, 1.0),
(2.0, 2.0, -1.0), (0.0, 2.0, -1.0), (1.0, 3.0, -1.0),
(-3.0, -1.0, -1.0), (-2.0, 0.0, -1.0), (-2.0, -2.0, -1.0),
(-1.0, -3.0, 1.0), (-3.0, -3.0, 1.0), (-3.0, -3.0, -1.0),
(-3.0, -3.0, 1.0), (-3.0, -1.0, 1.0), (-3.0, -1.0, -1.0),
(-2.0, 0.0, 1.0), (-2.0, 0.0, -1.0), (-3.0, -1.0, -1.0),
(-2.0, 0.0, -1.0), (-2.0, 0.0, 1.0), (-2.0, -2.0, 1.0),
(-2.0, -2.0, -1.0), (-2.0, -2.0, 1.0), (0.0, -2.0, 1.0),
(-1.0, -3.0, 1.0), (0.0, -2.0, 1.0), (-2.0, -2.0, 1.0),
(0.0, -2.0, -1.0), (0.0, -2.0, 1.0), (-1.0, -3.0, 1.0),
(-2.0, -2.0, 1.0), (-2.0, 0.0, 1.0), (-3.0, -1.0, 1.0),
(-2.0, -2.0, -1.0), (0.0, -2.0, -1.0), (-1.0, -3.0, -1.0),
)
class MyCustomShapeWidget(Gizmo):
bl_idname = "VIEW3D_GT_custom_shape_widget"
bl_target_properties = (
{"id": "offset", "type": 'FLOAT', "array_length": 1},
)
__slots__ = (
"custom_shape",
"init_mouse_y",
"init_value",
)
def _update_offset_matrix(self):
# offset behind the light
self.matrix_offset.col[3][2] = self.target_get_value("offset") / -10.0
def draw(self, context):
self._update_offset_matrix()
self.draw_custom_shape(self.custom_shape)
def draw_select(self, context, select_id):
self._update_offset_matrix()
self.draw_custom_shape(self.custom_shape, select_id=select_id)
def setup(self):
if not hasattr(self, "custom_shape"):
self.custom_shape = self.new_custom_shape('TRIS', custom_shape_verts)
def invoke(self, context, event):
self.init_mouse_y = event.mouse_y
self.init_value = self.target_get_value("offset")
return {'RUNNING_MODAL'}
def exit(self, context, cancel):
context.area.header_text_set(None)
if cancel:
self.target_set_value("offset", self.init_value)
def modal(self, context, event, tweak):
delta = (event.mouse_y - self.init_mouse_y) / 10.0
if 'SNAP' in tweak:
delta = round(delta)
if 'PRECISE' in tweak:
delta /= 10.0
value = self.init_value - delta
self.target_set_value("offset", value)
context.area.header_text_set("My Gizmo: {:.4f}".format(value))
return {'RUNNING_MODAL'}
class MyCustomShapeWidgetGroup(GizmoGroup):
bl_idname = "OBJECT_GGT_light_test"
bl_label = "Test Light Widget"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'3D', 'PERSISTENT'}
@classmethod
def poll(cls, context):
ob = context.object
return (ob and ob.type == 'LIGHT')
def setup(self, context):
# Assign the `offset` target property to the light energy.
ob = context.object
gz = self.gizmos.new(MyCustomShapeWidget.bl_idname)
gz.target_set_prop("offset", ob.data, "energy")
gz.color = 1.0, 0.5, 1.0
gz.alpha = 0.5
gz.color_highlight = 1.0, 1.0, 1.0
gz.alpha_highlight = 0.5
# units are large, so shrink to something more reasonable.
gz.scale_basis = 0.1
gz.use_draw_modal = True
self.energy_gizmo = gz
def refresh(self, context):
ob = context.object
gz = self.energy_gizmo
gz.matrix_basis = ob.matrix_world.normalized()
classes = (
MyCustomShapeWidget,
MyCustomShapeWidgetGroup,
)
for cls in classes:
bpy.utils.register_class(cls)

View File

@@ -0,0 +1,235 @@
# Example of an operator which uses gizmos to control its properties.
#
# Usage: Run this script, then in mesh edit-mode press F3
# to activate the operator "Select Side of Plane"
# The gizmos can then be used to adjust the plane in the 3D view.
#
import bpy
import bmesh
from bpy.types import (
Operator,
GizmoGroup,
)
from bpy.props import (
FloatVectorProperty,
)
def main(context, plane_co, plane_no):
obj = context.active_object
matrix = obj.matrix_world.copy()
me = obj.data
bm = bmesh.from_edit_mesh(me)
plane_dot = plane_no.dot(plane_co)
for v in bm.verts:
co = matrix @ v.co
v.select = (plane_no.dot(co) > plane_dot)
bm.select_flush_mode()
bmesh.update_edit_mesh(me)
class SelectSideOfPlane(Operator):
"""Select all vertices on one side of a plane defined by a location and a direction"""
bl_idname = "mesh.select_side_of_plane"
bl_label = "Select Side of Plane"
bl_options = {'REGISTER', 'UNDO'}
plane_co: FloatVectorProperty(
size=3,
default=(0, 0, 0),
)
plane_no: FloatVectorProperty(
size=3,
default=(0, 0, 1),
)
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def invoke(self, context, event):
if not self.properties.is_property_set("plane_co"):
self.plane_co = context.scene.cursor.location
if not self.properties.is_property_set("plane_no"):
if context.space_data.type == 'VIEW_3D':
rv3d = context.space_data.region_3d
view_inv = rv3d.view_matrix.to_3x3()
# view y axis
self.plane_no = view_inv[1].normalized()
self.execute(context)
if context.space_data.type == 'VIEW_3D':
wm = context.window_manager
wm.gizmo_group_type_ensure(SelectSideOfPlaneGizmoGroup.bl_idname)
return {'FINISHED'}
def execute(self, context):
from mathutils import Vector
main(context, Vector(self.plane_co), Vector(self.plane_no))
return {'FINISHED'}
# Gizmos for plane_co, plane_no
class SelectSideOfPlaneGizmoGroup(GizmoGroup):
bl_idname = "MESH_GGT_select_side_of_plane"
bl_label = "Side of Plane Gizmo"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'3D', 'EXCLUDE_MODAL'}
# Helper functions
@staticmethod
def my_target_operator(context):
wm = context.window_manager
op = wm.operators[-1] if wm.operators else None
if isinstance(op, SelectSideOfPlane):
return op
return None
@staticmethod
def my_view_orientation(context):
rv3d = context.space_data.region_3d
view_inv = rv3d.view_matrix.to_3x3()
return view_inv.normalized()
@classmethod
def poll(cls, context):
op = cls.my_target_operator(context)
if op is None:
wm = context.window_manager
wm.gizmo_group_type_unlink_delayed(SelectSideOfPlaneGizmoGroup.bl_idname)
return False
return True
def setup(self, context):
from mathutils import Matrix, Vector
# ----
# Move
def move_get_cb():
op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
return op.plane_co
def move_set_cb(value):
op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
op.plane_co = value
# XXX, this may change!
op.execute(context)
gz = self.gizmos.new("GIZMO_GT_move_3d")
gz.target_set_handler("offset", get=move_get_cb, set=move_set_cb)
gz.use_draw_value = True
gz.color = 0.8, 0.8, 0.8
gz.alpha = 0.5
gz.color_highlight = 1.0, 1.0, 1.0
gz.alpha_highlight = 1.0
gz.scale_basis = 0.2
self.gizmo_move = gz
# ----
# Dial
def direction_get_cb():
op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
no_a = self.gizmo_dial.matrix_basis.col[1].xyz
no_b = Vector(op.plane_no)
no_a = (no_a @ self.view_inv).xy.normalized()
no_b = (no_b @ self.view_inv).xy.normalized()
return no_a.angle_signed(no_b)
def direction_set_cb(value):
op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
matrix_rotate = Matrix.Rotation(-value, 3, self.rotate_axis)
no = matrix_rotate @ self.gizmo_dial.matrix_basis.col[1].xyz
op.plane_no = no
op.execute(context)
gz = self.gizmos.new("GIZMO_GT_dial_3d")
gz.target_set_handler("offset", get=direction_get_cb, set=direction_set_cb)
gz.draw_options = {'ANGLE_START_Y'}
gz.use_draw_value = True
gz.color = 0.8, 0.8, 0.8
gz.alpha = 0.5
gz.color_highlight = 1.0, 1.0, 1.0
gz.alpha_highlight = 1.0
self.gizmo_dial = gz
def draw_prepare(self, context):
from mathutils import Vector
view_inv = self.my_view_orientation(context)
self.view_inv = view_inv
self.rotate_axis = view_inv[2].xyz
self.rotate_up = view_inv[1].xyz
op = self.my_target_operator(context)
co = Vector(op.plane_co)
no = Vector(op.plane_no).normalized()
# Move
no_z = no
no_y = no_z.orthogonal()
no_x = no_z.cross(no_y)
matrix = self.gizmo_move.matrix_basis
matrix.identity()
matrix.col[0].xyz = no_x
matrix.col[1].xyz = no_y
matrix.col[2].xyz = no_z
# The location callback handles the location.
# `matrix.col[3].xyz = co`.
# Dial
no_z = self.rotate_axis
no_y = (no - (no.project(no_z))).normalized()
no_x = self.rotate_axis.cross(no_y)
matrix = self.gizmo_dial.matrix_basis
matrix.identity()
matrix.col[0].xyz = no_x
matrix.col[1].xyz = no_y
matrix.col[2].xyz = no_z
matrix.col[3].xyz = co
classes = (
SelectSideOfPlane,
SelectSideOfPlaneGizmoGroup,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,50 @@
# Example of a gizmo that activates an operator
# using the predefined dial gizmo to change the camera roll.
#
# Usage: Run this script and select a camera in the 3D view.
#
import bpy
from bpy.types import (
GizmoGroup,
)
class MyCameraWidgetGroup(GizmoGroup):
bl_idname = "OBJECT_GGT_test_camera"
bl_label = "Object Camera Test Widget"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'3D', 'PERSISTENT'}
@classmethod
def poll(cls, context):
ob = context.object
return (ob and ob.type == 'CAMERA')
def setup(self, context):
# Run an operator using the dial gizmo
ob = context.object
gz = self.gizmos.new("GIZMO_GT_dial_3d")
props = gz.target_set_operator("transform.rotate")
props.constraint_axis = False, False, True
props.orient_type = 'LOCAL'
props.release_confirm = True
gz.matrix_basis = ob.matrix_world.normalized()
gz.line_width = 3
gz.color = 0.8, 0.8, 0.8
gz.alpha = 0.5
gz.color_highlight = 1.0, 1.0, 1.0
gz.alpha_highlight = 1.0
self.roll_gizmo = gz
def refresh(self, context):
ob = context.object
gz = self.roll_gizmo
gz.matrix_basis = ob.matrix_world.normalized()
bpy.utils.register_class(MyCameraWidgetGroup)

View File

@@ -0,0 +1,59 @@
# Example of a group that lets you move an object in the 3D view
# using the default translate transform operator.
#
# Usage: Select and object and drag the Gizmo to
# move it in the 3D Viewport.
#
import bpy
from bpy.types import (
GizmoGroup,
)
class MyTranslateWidgetGroup(GizmoGroup):
bl_idname = "OBJECT_GGT_translate_test"
bl_label = "Test Translate Widget"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'PERSISTENT', 'SCALE'}
@classmethod
def poll(cls, context):
ob = context.object
return ob is not None
def draw_prepare(self, context):
region = context.region
# Place the Gizmo in the lower center of the 3D Viewport.
self.translate_gizmo.matrix_basis[0][3] = region.width / 2
self.translate_gizmo.matrix_basis[1][3] = region.height / 16
def setup(self, context):
gz = self.gizmos.new("GIZMO_GT_button_2d")
gz.target_set_operator("transform.translate")
gz.draw_options = {'BACKDROP', 'OUTLINE'}
gz.color = 0.0, 0.5, 1.0
gz.alpha = 0.2
gz.backdrop_fill_alpha = 0.1
gz.color_highlight = 1.0, 0.5, 0.0
gz.alpha_highlight = 0.8
gz.use_tooltip = True
gz.line_width = 1.5
# Same as buttons defined in C++ code.
gz.scale_basis = (80 * 0.35) / 2
# Show a dragging mouse cursor when hovering the gizmo.
gz.show_drag = True
# Can also use gz.icon_value to use a custom/generated preview icon.
gz.icon = 'EMPTY_ARROWS'
self.translate_gizmo = gz
bpy.utils.register_class(MyTranslateWidgetGroup)

View File

@@ -0,0 +1,47 @@
# Example of a group that edits a single property
# using the predefined gizmo arrow.
#
# Usage: Select a light in the 3D view and drag the arrow at its rear
# to change its energy value.
#
import bpy
from bpy.types import (
GizmoGroup,
)
class MyLightWidgetGroup(GizmoGroup):
bl_idname = "OBJECT_GGT_light_test"
bl_label = "Test Light Widget"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW'
bl_options = {'3D', 'PERSISTENT'}
@classmethod
def poll(cls, context):
ob = context.object
return (ob and ob.type == 'LIGHT')
def setup(self, context):
# Arrow gizmo has one `offset` property we can assign to the light energy.
ob = context.object
gz = self.gizmos.new("GIZMO_GT_arrow_3d")
gz.target_set_prop("offset", ob.data, "energy")
gz.matrix_basis = ob.matrix_world.normalized()
gz.draw_style = 'BOX'
gz.color = 1.0, 0.5, 0.0
gz.alpha = 0.5
gz.color_highlight = 1.0, 0.5, 1.0
gz.alpha_highlight = 0.5
self.energy_gizmo = gz
def refresh(self, context):
ob = context.object
gz = self.energy_gizmo
gz.matrix_basis = ob.matrix_world.normalized()
bpy.utils.register_class(MyLightWidgetGroup)

View File

@@ -0,0 +1,76 @@
import bpy
def write_some_data(context, filepath, use_some_setting):
print("running write_some_data...")
f = open(filepath, "w", encoding='utf-8')
f.write("Hello World {:s}".format(use_some_setting))
f.close()
return {'FINISHED'}
# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
class ExportSomeData(Operator, ExportHelper):
"""This appears in the tooltip of the operator and in the generated docs"""
bl_idname = "export_test.some_data" # Important since its how bpy.ops.import_test.some_data is constructed.
bl_label = "Export Some Data"
# ExportHelper mix-in class uses this.
filename_ext = ".txt"
filter_glob: StringProperty(
default="*.txt",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
use_setting: BoolProperty(
name="Example Boolean",
description="Example Tooltip",
default=True,
)
type: EnumProperty(
name="Example Enum",
description="Choose between two items",
items=(
('OPT_A', "First Option", "Description one"),
('OPT_B', "Second Option", "Description two"),
),
default='OPT_A',
)
def execute(self, context):
return write_some_data(context, self.filepath, self.use_setting)
# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")
# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access).
def register():
bpy.utils.register_class(ExportSomeData)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(ExportSomeData)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.export_test.some_data('INVOKE_DEFAULT')

View File

@@ -0,0 +1,79 @@
import bpy
def read_some_data(context, filepath, use_some_setting):
print("running read_some_data...")
f = open(filepath, "r", encoding="utf-8")
data = f.read()
f.close()
# Would normally load the data here.
print(data)
return {'FINISHED'}
# ImportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
class ImportSomeData(Operator, ImportHelper):
"""This appears in the tooltip of the operator and in the generated docs"""
bl_idname = "import_test.some_data" # important since its how bpy.ops.import_test.some_data is constructed
bl_label = "Import Some Data"
# ImportHelper mix-in class uses this.
filename_ext = ".txt"
filter_glob: StringProperty(
default="*.txt",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
use_setting: BoolProperty(
name="Example Boolean",
description="Example Tooltip",
default=True,
)
type: EnumProperty(
name="Example Enum",
description="Choose between two items",
items=(
('OPT_A', "First Option", "Description one"),
('OPT_B', "Second Option", "Description two"),
),
default='OPT_A',
)
def execute(self, context):
return read_some_data(context, self.filepath, self.use_setting)
# Only needed if you want to add into a dynamic menu.
def menu_func_import(self, context):
self.layout.operator(ImportSomeData.bl_idname, text="Text Import Operator")
# Register and add to the "file selector" menu (required to use F3 search "Text Import Operator" for quick access).
def register():
bpy.utils.register_class(ImportSomeData)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.utils.unregister_class(ImportSomeData)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.import_test.some_data('INVOKE_DEFAULT')

View File

@@ -0,0 +1,116 @@
import bpy
import bmesh
from bpy_extras.object_utils import AddObjectHelper
from bpy.props import (
FloatProperty,
)
def add_box(width, height, depth):
"""
This function takes inputs and returns vertex and face arrays.
no actual mesh data creation is done here.
"""
verts = [
(+1.0, +1.0, -1.0),
(+1.0, -1.0, -1.0),
(-1.0, -1.0, -1.0),
(-1.0, +1.0, -1.0),
(+1.0, +1.0, +1.0),
(+1.0, -1.0, +1.0),
(-1.0, -1.0, +1.0),
(-1.0, +1.0, +1.0),
]
faces = [
(0, 1, 2, 3),
(4, 7, 6, 5),
(0, 4, 5, 1),
(1, 5, 6, 2),
(2, 6, 7, 3),
(4, 0, 3, 7),
]
# Apply size.
for i, v in enumerate(verts):
verts[i] = v[0] * width, v[1] * depth, v[2] * height
return verts, faces
class AddBox(bpy.types.Operator, AddObjectHelper):
"""Add a simple box mesh"""
bl_idname = "mesh.primitive_box_add"
bl_label = "Add Box"
bl_options = {'REGISTER', 'UNDO'}
width: FloatProperty(
name="Width",
description="Box Width",
min=0.01, max=100.0,
default=1.0,
)
height: FloatProperty(
name="Height",
description="Box Height",
min=0.01, max=100.0,
default=1.0,
)
depth: FloatProperty(
name="Depth",
description="Box Depth",
min=0.01, max=100.0,
default=1.0,
)
def execute(self, context):
verts_loc, faces = add_box(
self.width,
self.height,
self.depth,
)
mesh = bpy.data.meshes.new("Box")
bm = bmesh.new()
for v_co in verts_loc:
bm.verts.new(v_co)
bm.verts.ensure_lookup_table()
for f_idx in faces:
bm.faces.new([bm.verts[i] for i in f_idx])
bm.to_mesh(mesh)
mesh.update()
# Add the mesh as an object into the scene with this utility module.
from bpy_extras import object_utils
object_utils.object_data_add(context, mesh, operator=self)
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(AddBox.bl_idname, icon='MESH_CUBE')
# Register and add to the "add mesh" menu (required to use F3 search "Add Box" for quick access).
def register():
bpy.utils.register_class(AddBox)
bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
def unregister():
bpy.utils.unregister_class(AddBox)
bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.mesh.primitive_box_add()

View File

@@ -0,0 +1,56 @@
import bpy
import bmesh
def main(context):
obj = context.active_object
me = obj.data
bm = bmesh.from_edit_mesh(me)
uv_layer = bm.loops.layers.uv.verify()
# Adjust UV coordinates.
for face in bm.faces:
for loop in face.loops:
loop_uv = loop[uv_layer]
# Use XY position of the vertex as a uv coordinate.
loop_uv.uv = loop.vert.co.xy
bmesh.update_edit_mesh(me)
class UvOperator(bpy.types.Operator):
"""UV Operator description"""
bl_idname = "uv.simple_operator"
bl_label = "Simple UV Operator"
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == 'MESH' and obj.mode == 'EDIT'
def execute(self, context):
main(context)
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(UvOperator.bl_idname, text="Simple UV Operator")
# Register and add to the "UV" menu (required to also use F3 search "Simple UV Operator" for quick access).
def register():
bpy.utils.register_class(UvOperator)
bpy.types.IMAGE_MT_uvs.append(menu_func)
def unregister():
bpy.utils.unregister_class(UvOperator)
bpy.types.IMAGE_MT_uvs.remove(menu_func)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.uv.simple_operator()

View File

@@ -0,0 +1,58 @@
import bpy
from bpy.props import IntProperty, FloatProperty
class ModalOperator(bpy.types.Operator):
"""Move an object with the mouse, example"""
bl_idname = "object.modal_operator"
bl_label = "Simple Modal Operator"
first_mouse_x: IntProperty()
first_value: FloatProperty()
def modal(self, context, event):
if event.type == 'MOUSEMOVE':
delta = self.first_mouse_x - event.mouse_x
context.object.location.x = self.first_value + delta * 0.01
elif event.type == 'LEFTMOUSE':
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
context.object.location.x = self.first_value
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.object:
self.first_mouse_x = event.mouse_x
self.first_value = context.object.location.x
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "No active object, could not finish")
return {'CANCELLED'}
def menu_func(self, context):
self.layout.operator(ModalOperator.bl_idname, text=ModalOperator.bl_label)
# Register and add to the "view" menu (required to also use F3 search "Simple Modal Operator" for quick access).
def register():
bpy.utils.register_class(ModalOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.utils.unregister_class(ModalOperator)
bpy.types.VIEW3D_MT_object.remove(menu_func)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.object.modal_operator('INVOKE_DEFAULT')

View File

@@ -0,0 +1,83 @@
import bpy
import blf
import gpu
from gpu_extras.batch import batch_for_shader
def draw_callback_px(self, context):
print("mouse points", len(self.mouse_path))
font_id = 0 # XXX, need to find out how best to get this.
# draw some text
blf.position(font_id, 15, 30, 0)
blf.size(font_id, 20.0)
blf.draw(font_id, "Hello Word " + str(len(self.mouse_path)))
# 50% alpha, 2 pixel width line
gpu.state.blend_set('ALPHA')
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.uniform_float("color", (0.0, 0.0, 0.0, 0.5))
shader.uniform_float("viewportSize", (context.area.width, context.area.height))
shader.uniform_float('lineWidth', 2.0)
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": self.mouse_path})
batch.draw(shader)
# restore gpu defaults
gpu.state.blend_set('NONE')
class ModalDrawOperator(bpy.types.Operator):
"""Draw a line with the mouse"""
bl_idname = "view3d.modal_draw_operator"
bl_label = "Simple Modal View3D Operator"
def modal(self, context, event):
context.area.tag_redraw()
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'LEFTMOUSE':
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'VIEW_3D':
# The arguments we pass 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(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
def menu_func(self, context):
self.layout.operator(ModalDrawOperator.bl_idname, text="Modal Draw Operator")
# Register and add to the "view" menu (required to also use F3 search "Modal Draw Operator" for quick access).
def register():
bpy.utils.register_class(ModalDrawOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
def unregister():
bpy.utils.unregister_class(ModalDrawOperator)
bpy.types.VIEW3D_MT_view.remove(menu_func)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,61 @@
import bpy
class ModalTimerOperator(bpy.types.Operator):
"""Operator which runs itself from a timer"""
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
_theme = None
_original_color = None
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
# Change theme color, silly!
color = self._theme.view_3d.space.gradients.high_gradient
color.s = 1.0
color.h += 0.01
return {'PASS_THROUGH'}
def execute(self, context):
# Store the original color to avoid altering user settings
self._theme = context.preferences.themes[0]
color = self._theme.view_3d.space.gradients.high_gradient
self._original_color = color[:]
wm = context.window_manager
self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self._theme.view_3d.space.gradients.high_gradient = self._original_color
def menu_func(self, context):
self.layout.operator(ModalTimerOperator.bl_idname, text=ModalTimerOperator.bl_label)
def register():
bpy.utils.register_class(ModalTimerOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
# Register and add to the "view" menu (required to also use F3 search "Modal Timer Operator" for quick access).
def unregister():
bpy.utils.unregister_class(ModalTimerOperator)
bpy.types.VIEW3D_MT_view.remove(menu_func)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.wm.modal_timer_operator()

View File

@@ -0,0 +1,77 @@
import bpy
from mathutils import Vector
from bpy.props import FloatVectorProperty
class ViewOperator(bpy.types.Operator):
"""Translate the view using mouse events"""
bl_idname = "view3d.modal_operator"
bl_label = "Simple View Operator"
offset: FloatVectorProperty(
name="Offset",
size=3,
)
def execute(self, context):
v3d = context.space_data
rv3d = v3d.region_3d
rv3d.view_location = self._initial_location + Vector(self.offset)
def modal(self, context, event):
v3d = context.space_data
rv3d = v3d.region_3d
if event.type == 'MOUSEMOVE':
self.offset = (self._initial_mouse - Vector((event.mouse_x, event.mouse_y, 0.0))) * 0.02
self.execute(context)
context.area.header_text_set("Offset {:.4f} {:.4f} {:.4f}".format(*self.offset))
elif event.type == 'LEFTMOUSE':
context.area.header_text_set(None)
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
rv3d.view_location = self._initial_location
context.area.header_text_set(None)
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.space_data.type == 'VIEW_3D':
v3d = context.space_data
rv3d = v3d.region_3d
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
self._initial_mouse = Vector((event.mouse_x, event.mouse_y, 0.0))
self._initial_location = rv3d.view_location.copy()
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "Active space must be a View3d")
return {'CANCELLED'}
def menu_func(self, context):
self.layout.operator(ViewOperator.bl_idname, text="Simple View Modal Operator")
# Register and add to the "view" menu (required to also use F3 search "Simple View Modal Operator" for quick access).
def register():
bpy.utils.register_class(ViewOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
def unregister():
bpy.utils.unregister_class(ViewOperator)
bpy.types.VIEW3D_MT_view.remove(menu_func)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,115 @@
import bpy
from bpy_extras import view3d_utils
def main(context, event):
"""Run this function on left mouse, execute the ray cast"""
# get the context arguments
scene = context.scene
region = context.region
rv3d = context.region_data
coord = event.mouse_region_x, event.mouse_region_y
# get the ray from the viewport and mouse
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
ray_target = ray_origin + view_vector
def visible_objects_and_duplis():
"""Loop over (object, matrix) pairs (mesh only)"""
depsgraph = context.evaluated_depsgraph_get()
for dup in depsgraph.object_instances:
if dup.is_instance: # Real dupli instance
obj = dup.instance_object
yield (obj, dup.matrix_world.copy())
else: # Usual object
obj = dup.object
yield (obj, obj.matrix_world.copy())
def obj_ray_cast(obj, matrix):
"""Wrapper for ray casting that moves the ray into object space"""
# get the ray relative to the object
matrix_inv = matrix.inverted()
ray_origin_obj = matrix_inv @ ray_origin
ray_target_obj = matrix_inv @ ray_target
ray_direction_obj = ray_target_obj - ray_origin_obj
# cast the ray
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success:
return location, normal, face_index
else:
return None, None, None
# cast rays and find the closest object
best_length_squared = -1.0
best_obj = None
for obj, matrix in visible_objects_and_duplis():
if obj.type == 'MESH':
hit, normal, face_index = obj_ray_cast(obj, matrix)
if hit is not None:
hit_world = matrix @ hit
scene.cursor.location = hit_world
length_squared = (hit_world - ray_origin).length_squared
if best_obj is None or length_squared < best_length_squared:
best_length_squared = length_squared
best_obj = obj
# now we have the object under the mouse cursor,
# we could do lots of stuff but for the example just select.
if best_obj is not None:
# for selection etc. we need the original object,
# evaluated objects are not in view-layer.
best_original = best_obj.original
best_original.select_set(True)
context.view_layer.objects.active = best_original
class ViewOperatorRayCast(bpy.types.Operator):
"""Modal object selection with a ray cast"""
bl_idname = "view3d.modal_operator_raycast"
bl_label = "Ray-cast View Operator"
def modal(self, context, event):
if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
# allow navigation
return {'PASS_THROUGH'}
elif event.type == 'LEFTMOUSE':
main(context, event)
return {'RUNNING_MODAL'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.space_data.type == 'VIEW_3D':
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "Active space must be a View3d")
return {'CANCELLED'}
def menu_func(self, context):
self.layout.operator(ViewOperatorRayCast.bl_idname, text="Raycast View Modal Operator")
# Register and add to the "view" menu (required to also use F3 search "Ray-cast View Modal Operator" for quick access).
def register():
bpy.utils.register_class(ViewOperatorRayCast)
bpy.types.VIEW3D_MT_view.append(menu_func)
def unregister():
bpy.utils.unregister_class(ViewOperatorRayCast)
bpy.types.VIEW3D_MT_view.remove(menu_func)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,66 @@
import bpy
def main(operator, context):
space = context.space_data
node_tree = space.node_tree
node_active = context.active_node
node_selected = context.selected_nodes
# now we have the context, perform a simple operation
if node_active in node_selected:
node_selected.remove(node_active)
if len(node_selected) != 1:
operator.report({'ERROR'}, "2 nodes must be selected")
return
node_other, = node_selected
# now we have 2 nodes to operate on
if not node_active.inputs:
operator.report({'ERROR'}, "Active node has no inputs")
return
if not node_other.outputs:
operator.report({'ERROR'}, "Selected node has no outputs")
return
socket_in = node_active.inputs[0]
socket_out = node_other.outputs[0]
# add a link between the two nodes
node_link = node_tree.links.new(socket_in, socket_out)
class NodeOperator(bpy.types.Operator):
"""Tooltip"""
bl_idname = "node.simple_operator"
bl_label = "Simple Node Operator"
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == 'NODE_EDITOR'
def execute(self, context):
main(self, context)
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(NodeOperator.bl_idname, text=NodeOperator.bl_label)
# Register and add to the "Node" menu (required to also use F3 search "Simple Node Operator" for quick access).
def register():
bpy.utils.register_class(NodeOperator)
bpy.types.NODE_MT_node.append(menu_func)
def unregister():
bpy.utils.unregister_class(NodeOperator)
bpy.types.NODE_MT_node.remove(menu_func)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,42 @@
import bpy
def main(context):
for ob in context.scene.objects:
print(ob)
class SimpleOperator(bpy.types.Operator):
"""Tooltip"""
bl_idname = "object.simple_operator"
bl_label = "Simple Object Operator"
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
main(context)
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(SimpleOperator.bl_idname, text=SimpleOperator.bl_label)
# Register and add to the "object" menu (required to also use F3 search "Simple Object Operator" for quick access).
def register():
bpy.utils.register_class(SimpleOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.utils.unregister_class(SimpleOperator)
bpy.types.VIEW3D_MT_object.remove(menu_func)
if __name__ == "__main__":
register()
# Test call.
bpy.ops.object.simple_operator()

View File

@@ -0,0 +1,29 @@
import bpy
class MyAssetShelf(bpy.types.AssetShelf):
bl_space_type = 'VIEW_3D'
bl_idname = "VIEW3D_AST_my_asset_shelf"
# Tell the shelf which data-block types to show. It is highly recommended
# to use these filters, as they avoid slowdowns when there are many assets
# of irrelevant data-block types.
# If no filter is set, all data-block types will show.
filter_material = True
filter_object = True
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def register():
bpy.utils.register_class(MyAssetShelf)
def unregister():
bpy.utils.unregister_class(MyAssetShelf)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,44 @@
import bpy
class MESH_UL_mylist(bpy.types.UIList):
# Constants (flags)
# Be careful not to shadow FILTER_ITEM (i.e. UIList().bitflag_filter_item)!
# E.g. VGROUP_EMPTY = 1 << 0
# Custom properties, saved with .blend file. E.g.
# use_filter_empty: bpy.props.BoolProperty(
# name="Filter Empty", default=False, options=set(),
# description="Whether to filter empty vertex groups",
# )
# Called for each drawn item.
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
pass
# Called once to draw filtering/reordering options.
def draw_filter(self, context, layout):
# Nothing much to say here, it's usual UI code...
pass
# Called once to filter/reorder items.
def filter_items(self, context, data, propname):
# This function gets the collection property (as the usual tuple (data, propname)), and must return two lists:
# * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the
# matching item as filtered (i.e. to be shown). The upper 16 bits (including self.bitflag_filter_item) are
# reserved for internal use, the lower 16 bits are free for custom use. Here we use the first bit to mark
# VGROUP_EMPTY.
# * The second one is for reordering, it must return a list containing the new indices of the items (which
# gives us a mapping org_idx -> new_idx).
# Please note that the default `UI_UL_list` defines helper functions for common tasks
# (see its doc for more info).
# If you do not make filtering and/or ordering, return empty list(s)
# (this will be more efficient than returning full lists doing nothing!).
# Default return values.
flt_flags = []
flt_neworder = []
# Do filtering/reordering here...
return flt_flags, flt_neworder

View File

@@ -0,0 +1,47 @@
import bpy
from bl_ui.generic_ui_list import draw_ui_list
class MyPropGroup(bpy.types.PropertyGroup):
name: bpy.props.StringProperty()
class MyPanel(bpy.types.Panel):
bl_label = "My Label"
bl_idname = "SCENE_PT_list_demo"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Category"
def draw(self, context):
layout = self.layout
draw_ui_list(
layout,
context,
list_path="scene.my_list",
active_index_path="scene.my_list_active_index",
unique_id="my_list_id",
)
classes = [
MyPropGroup,
MyPanel,
]
class_register, class_unregister = bpy.utils.register_classes_factory(classes)
def register():
class_register()
bpy.types.Scene.my_list = bpy.props.CollectionProperty(type=MyPropGroup)
bpy.types.Scene.my_list_active_index = bpy.props.IntProperty()
def unregister():
class_unregister()
del bpy.types.Scene.my_list
del bpy.types.Scene.my_list_active_index
register()

View File

@@ -0,0 +1,70 @@
import bpy
class MATERIAL_UL_matslots_example(bpy.types.UIList):
# The draw_item function is called for each item of the collection that is visible in the list.
# data is the RNA object containing the collection,
# item is the current drawn item of the collection,
# icon is the "computed" icon for the item (as an integer, because some objects like materials or textures
# have custom icons ID, which are not available as enum items).
# active_data is the RNA object containing the active property for the collection (i.e. integer pointing to the
# active item of the collection).
# active_propname is the name of the active property (use 'getattr(active_data, active_propname)').
# index is index of the current item in the collection.
# flt_flag is the result of the filtering process for this item.
# Note: as index and flt_flag are optional arguments, you do not have to use/declare them here if you don't
# need them.
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
slot = item
ma = slot.material
# You should always start your row layout by a label (icon + text), or a non-embossed text field,
# this will also make the row easily selectable in the list! The later also enables ctrl-click rename.
# We use icon_value of label, as our given icon is an integer value, not an enum ID.
# Note "data" names should never be translated!
if ma:
layout.prop(ma, "name", text="", emboss=False, icon_value=icon)
else:
layout.label(text="", translate=False, icon_value=icon)
# And now we can use this list everywhere in Blender. Here is a small example panel.
class UIListPanelExample(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "UIList Panel"
bl_idname = "OBJECT_PT_ui_list_example"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
# `template_list` now takes two new arguments.
# The first one is the identifier of the registered UIList to use (if you want only the default list,
# with no custom draw code, use "UI_UL_list").
layout.template_list("MATERIAL_UL_matslots_example", "", obj, "material_slots", obj, "active_material_index")
# The second one can usually be left as an empty string.
# It's an additional ID used to distinguish lists in case you
# use the same list several times in a given area.
layout.template_list(
"MATERIAL_UL_matslots_example", "compact", obj, "material_slots",
obj, "active_material_index", type='COMPACT',
)
def register():
bpy.utils.register_class(MATERIAL_UL_matslots_example)
bpy.utils.register_class(UIListPanelExample)
def unregister():
bpy.utils.unregister_class(MATERIAL_UL_matslots_example)
bpy.utils.unregister_class(UIListPanelExample)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,50 @@
import bpy
class CustomMenu(bpy.types.Menu):
bl_label = "Custom Menu"
bl_idname = "OBJECT_MT_custom_menu"
def draw(self, context):
layout = self.layout
layout.operator("wm.open_mainfile")
layout.operator("wm.save_as_mainfile").copy = True
layout.operator("object.shade_smooth")
layout.label(text="Hello world!", icon='WORLD_DATA')
# use an operator enum property to populate a sub-menu
layout.operator_menu_enum(
"object.select_by_type",
property="type",
text="Select All by Type",
)
# call another menu
layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"
def draw_item(self, context):
layout = self.layout
layout.menu(CustomMenu.bl_idname)
def register():
bpy.utils.register_class(CustomMenu)
# lets add ourselves to the main header
bpy.types.INFO_HT_header.append(draw_item)
def unregister():
bpy.utils.unregister_class(CustomMenu)
bpy.types.INFO_HT_header.remove(draw_item)
if __name__ == "__main__":
register()
# The menu can also be called from scripts
bpy.ops.wm.call_menu(name=CustomMenu.bl_idname)

View File

@@ -0,0 +1,27 @@
import bpy
class SimpleCustomMenu(bpy.types.Menu):
bl_label = "Simple Custom Menu"
bl_idname = "OBJECT_MT_simple_custom_menu"
def draw(self, context):
layout = self.layout
layout.operator("wm.open_mainfile")
layout.operator("wm.save_as_mainfile")
def register():
bpy.utils.register_class(SimpleCustomMenu)
def unregister():
bpy.utils.unregister_class(SimpleCustomMenu)
if __name__ == "__main__":
register()
# The menu can also be called from scripts
bpy.ops.wm.call_menu(name=SimpleCustomMenu.bl_idname)

View File

@@ -0,0 +1,73 @@
import bpy
class LayoutDemoPanel(bpy.types.Panel):
"""Creates a Panel in the scene context of the properties editor"""
bl_label = "Layout Demo"
bl_idname = "SCENE_PT_layout"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
def draw(self, context):
layout = self.layout
scene = context.scene
# Create a simple row.
layout.label(text=" Simple Row:")
row = layout.row()
row.prop(scene, "frame_start")
row.prop(scene, "frame_end")
# Create an row where the buttons are aligned to each other.
layout.label(text=" Aligned Row:")
row = layout.row(align=True)
row.prop(scene, "frame_start")
row.prop(scene, "frame_end")
# Create two columns, by using a split layout.
split = layout.split()
# First column
col = split.column()
col.label(text="Column One:")
col.prop(scene, "frame_end")
col.prop(scene, "frame_start")
# Second column, aligned
col = split.column(align=True)
col.label(text="Column Two:")
col.prop(scene, "frame_start")
col.prop(scene, "frame_end")
# Big render button
layout.label(text="Big Button:")
row = layout.row()
row.scale_y = 3.0
row.operator("render.render")
# Different sizes in a row
layout.label(text="Different button sizes:")
row = layout.row(align=True)
row.operator("render.render")
sub = row.row()
sub.scale_x = 2.0
sub.operator("render.render")
row.operator("render.render")
def register():
bpy.utils.register_class(LayoutDemoPanel)
def unregister():
bpy.utils.unregister_class(LayoutDemoPanel)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,38 @@
import bpy
class HelloWorldPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "Hello World Panel"
bl_idname = "OBJECT_PT_hello"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
row = layout.row()
row.label(text="Hello world!", icon='WORLD_DATA')
row = layout.row()
row.label(text="Active object is: " + obj.name)
row = layout.row()
row.prop(obj, "name")
row = layout.row()
row.operator("mesh.primitive_cube_add")
def register():
bpy.utils.register_class(HelloWorldPanel)
def unregister():
bpy.utils.unregister_class(HelloWorldPanel)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,31 @@
import bpy
from bpy.types import Menu
# spawn an edit mode selection pie (run while object is in edit mode to get a valid output)
class VIEW3D_MT_PIE_template(Menu):
# label is displayed at the center of the pie menu.
bl_label = "Select Mode"
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
# operator_enum will just spread all available options
# for the type enum of the operator on the pie
pie.operator_enum("mesh.select_mode", "type")
def register():
bpy.utils.register_class(VIEW3D_MT_PIE_template)
def unregister():
bpy.utils.unregister_class(VIEW3D_MT_PIE_template)
if __name__ == "__main__":
register()
bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_template")

View File

@@ -0,0 +1,76 @@
# This sample script demonstrates how to place a custom icon on a button or
# menu entry.
#
# IMPORTANT NOTE: if you run this sample, there will be no icon in the button
# You need to replace the image path with a real existing one.
# For distributable scripts, it is recommended to place the icons inside the
# addon folder and access it relative to the py script file for portability
#
#
# Other use cases for UI-previews:
# - provide a fixed list of previews to select from
# - provide a dynamic list of preview (eg. calculated from reading a directory)
#
# For the above use cases, see the template `ui_previews_dynamic_enum.py`.
import os
import bpy
class PreviewsExamplePanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "Previews Example Panel"
bl_idname = "OBJECT_PT_previews"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
pcoll = preview_collections["main"]
row = layout.row()
my_icon = pcoll["my_icon"]
row.operator("render.render", icon_value=my_icon.icon_id)
# my_icon.icon_id can be used in any UI function that accepts
# icon_value # try also setting text=""
# to get an icon only operator button
# We can store multiple preview collections here,
# however in this example we only store "main"
preview_collections = {}
def register():
# Note that preview collections returned by bpy.utils.previews
# are regular py objects - you can use them to store custom data.
import bpy.utils.previews
pcoll = bpy.utils.previews.new()
# path to the folder where the icon is
# the path is calculated relative to this py file inside the addon folder
my_icons_dir = os.path.join(os.path.dirname(__file__), "icons")
# load a preview thumbnail of a file and store in the previews collection
pcoll.load("my_icon", os.path.join(my_icons_dir, "icon-image.png"), 'IMAGE')
preview_collections["main"] = pcoll
bpy.utils.register_class(PreviewsExamplePanel)
def unregister():
for pcoll in preview_collections.values():
bpy.utils.previews.remove(pcoll)
preview_collections.clear()
bpy.utils.unregister_class(PreviewsExamplePanel)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,141 @@
# This sample script demonstrates a dynamic EnumProperty with custom icons.
# The EnumProperty is populated dynamically with thumbnails of the contents of
# a chosen directory in 'enum_previews_from_directory_items'.
# Then, the same enum is displayed with different interfaces. Note that the
# generated icon previews do not have Blender IDs, which means that they can
# not be used with UILayout templates that require IDs,
# such as template_list and template_ID_preview.
#
# Other use cases:
# - make a fixed list of enum_items instead of calculating them in a function
# - generate isolated thumbnails to use as custom icons in buttons
# and menu items
#
# For custom icons, see the template "ui_previews_custom_icon.py".
#
# For distributable scripts, it is recommended to place the icons inside the
# script directory and access it relative to the py script file for portability:
#
# os.path.join(os.path.dirname(__file__), "images")
import os
import bpy
def enum_previews_from_directory_items(self, context):
"""EnumProperty callback"""
enum_items = []
if context is None:
return enum_items
wm = context.window_manager
directory = wm.my_previews_dir
# Get the preview collection (defined in register func).
pcoll = preview_collections["main"]
if directory == pcoll.my_previews_dir:
return pcoll.my_previews
print("Scanning directory:", directory)
if directory and os.path.exists(directory):
# Scan the directory for `*.png` files
image_paths = []
for fn in os.listdir(directory):
if fn.lower().endswith(".png"):
image_paths.append(fn)
for i, name in enumerate(image_paths):
# generates a thumbnail preview for a file.
filepath = os.path.join(directory, name)
icon = pcoll.get(name)
if not icon:
thumb = pcoll.load(name, filepath, 'IMAGE')
else:
thumb = pcoll[name]
enum_items.append((name, name, "", thumb.icon_id, i))
pcoll.my_previews = enum_items
pcoll.my_previews_dir = directory
return pcoll.my_previews
class PreviewsExamplePanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "Previews Example Panel"
bl_idname = "OBJECT_PT_previews"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
wm = context.window_manager
row = layout.row()
row.prop(wm, "my_previews_dir")
row = layout.row()
row.template_icon_view(wm, "my_previews")
row = layout.row()
row.prop(wm, "my_previews")
# We can store multiple preview collections here,
# however in this example we only store "main"
preview_collections = {}
def register():
from bpy.types import WindowManager
from bpy.props import (
StringProperty,
EnumProperty,
)
WindowManager.my_previews_dir = StringProperty(
name="Folder Path",
subtype='DIR_PATH',
default=""
)
WindowManager.my_previews = EnumProperty(
items=enum_previews_from_directory_items,
)
# Note that preview collections returned by bpy.utils.previews
# are regular Python objects - you can use them to store custom data.
#
# This is especially useful here, since:
# - It avoids us regenerating the whole enum over and over.
# - It can store enum_items' strings
# (remember you have to keep those strings somewhere in py,
# else they get freed and Blender references invalid memory!).
import bpy.utils.previews
pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = ""
pcoll.my_previews = ()
preview_collections["main"] = pcoll
bpy.utils.register_class(PreviewsExamplePanel)
def unregister():
from bpy.types import WindowManager
del WindowManager.my_previews
for pcoll in preview_collections.values():
bpy.utils.previews.remove(pcoll)
preview_collections.clear()
bpy.utils.unregister_class(PreviewsExamplePanel)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,91 @@
# This example adds an object mode tool to the toolbar.
# This is just the circle-select and lasso tools tool.
import bpy
from bpy.types import WorkSpaceTool
class MyTool(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
# The prefix of the idname should be your add-on name.
bl_idname = "my_template.my_circle_select"
bl_label = "My Circle Select"
bl_description = (
"This is a tooltip\n"
"with multiple lines"
)
bl_icon = "ops.generic.select_circle"
bl_widget = None
bl_keymap = (
("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS'},
{"properties": [("wait_for_input", False)]}),
("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'SUB'), ("wait_for_input", False)]}),
)
def draw_settings(context, layout, tool):
props = tool.operator_properties("view3d.select_circle")
layout.prop(props, "mode")
layout.prop(props, "radius")
class MyOtherTool(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_idname = "my_template.my_other_select"
bl_label = "My Lasso Tool Select"
bl_description = (
"This is a tooltip\n"
"with multiple lines"
)
bl_icon = "ops.generic.select_lasso"
bl_widget = None
bl_keymap = (
("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'SUB')]}),
)
def draw_settings(context, layout, tool):
props = tool.operator_properties("view3d.select_lasso")
layout.prop(props, "mode")
class MyWidgetTool(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_idname = "my_template.my_gizmo_translate"
bl_label = "My Gizmo Tool"
bl_description = "Short description"
bl_icon = "ops.transform.transform"
bl_widget = "VIEW3D_GGT_tool_generic_handle_free"
bl_widget_properties = [
("radius", 75.0),
("backdrop_fill_alpha", 0.0),
]
bl_keymap = (
("transform.transform", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
)
def draw_settings(context, layout, tool):
props = tool.operator_properties("transform.transform")
layout.prop(props, "mode")
def register():
bpy.utils.register_tool(MyTool, after={"builtin.scale_cage"}, separator=True, group=True)
bpy.utils.register_tool(MyOtherTool, after={MyTool.bl_idname})
bpy.utils.register_tool(MyWidgetTool, after={MyTool.bl_idname})
def unregister():
bpy.utils.unregister_tool(MyTool)
bpy.utils.unregister_tool(MyOtherTool)
bpy.utils.unregister_tool(MyWidgetTool)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,84 @@
# To make this add-on installable, create an extension with it:
# https://docs.blender.org/manual/en/latest/advanced/extensions/getting_started.html
import bpy
from bpy.types import Operator
from bpy.props import FloatVectorProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from mathutils import Vector
def add_object(self, context):
scale_x = self.scale.x
scale_y = self.scale.y
verts = [
Vector((-1 * scale_x, 1 * scale_y, 0)),
Vector((1 * scale_x, 1 * scale_y, 0)),
Vector((1 * scale_x, -1 * scale_y, 0)),
Vector((-1 * scale_x, -1 * scale_y, 0)),
]
edges = []
faces = [[0, 1, 2, 3]]
mesh = bpy.data.meshes.new(name="New Object Mesh")
mesh.from_pydata(verts, edges, faces)
# useful for development when the mesh may be invalid.
# mesh.validate(verbose=True)
object_data_add(context, mesh, operator=self)
class OBJECT_OT_add_object(Operator, AddObjectHelper):
"""Create a new Mesh Object"""
bl_idname = "mesh.add_object"
bl_label = "Add Mesh Object"
bl_options = {'REGISTER', 'UNDO'}
scale: FloatVectorProperty(
name="scale",
default=(1.0, 1.0, 1.0),
subtype='TRANSLATION',
description="scaling",
)
def execute(self, context):
add_object(self, context)
return {'FINISHED'}
# Registration
def add_object_button(self, context):
self.layout.operator(
OBJECT_OT_add_object.bl_idname,
text="Add Object",
icon='PLUGIN',
)
# This allows you to right click on a button and link to documentation
def add_object_manual_map():
url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
url_manual_mapping = (
("bpy.ops.mesh.add_object", "scene_layout/object/types.html"),
)
return url_manual_prefix, url_manual_mapping
def register():
bpy.utils.register_class(OBJECT_OT_add_object)
bpy.utils.register_manual_map(add_object_manual_map)
bpy.types.VIEW3D_MT_mesh_add.append(add_object_button)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_add_object)
bpy.utils.unregister_manual_map(add_object_manual_map)
bpy.types.VIEW3D_MT_mesh_add.remove(add_object_button)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,118 @@
# This script is an example of how you can run blender from the command line
# (in background mode with no interface) to automate tasks, in this example it
# creates a text object, camera and light, then renders and/or saves it.
# This example also shows how you can parse command line options to scripts.
#
# Example usage for this test.
# blender --background --factory-startup --python $HOME/background_job.py -- \
# --text="Hello World" \
# --render="/tmp/hello" \
# --save="/tmp/hello.blend"
#
# Notice:
# `--factory-startup` is used to avoid the user default settings from
# interfering with automated scene generation.
#
# `--` causes blender to ignore all following arguments so python can use them.
#
# See blender --help for details.
import bpy
def example_function(text, save_path, render_path):
# Clear existing objects.
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
txt_data = bpy.data.curves.new(name="MyText", type='FONT')
# Text Object
txt_ob = bpy.data.objects.new(name="MyText", object_data=txt_data)
scene.collection.objects.link(txt_ob) # add the data to the scene as an object
txt_data.body = text # the body text to the command line arg given
txt_data.align_x = 'CENTER' # center text
# Camera
cam_data = bpy.data.cameras.new("MyCam")
cam_ob = bpy.data.objects.new(name="MyCam", object_data=cam_data)
scene.collection.objects.link(cam_ob) # instance the camera object in the scene
scene.camera = cam_ob # set the active camera
cam_ob.location = 0.0, 0.0, 10.0
# Light
light_data = bpy.data.lights.new("MyLight", 'POINT')
light_ob = bpy.data.objects.new(name="MyLight", object_data=light_data)
scene.collection.objects.link(light_ob)
light_ob.location = 2.0, 2.0, 5.0
bpy.context.view_layer.update()
if save_path:
bpy.ops.wm.save_as_mainfile(filepath=save_path)
if render_path:
render = scene.render
render.use_file_extension = True
render.filepath = render_path
bpy.ops.render.render(write_still=True)
def main():
import sys # to get command line args
import argparse # to parse options for us and print a nice help message
# get the args passed to blender after "--", all of which are ignored by
# blender so scripts may receive their own arguments
argv = sys.argv
if "--" not in argv:
argv = [] # as if no args are passed
else:
argv = argv[argv.index("--") + 1:] # get all args after "--"
# When --help or no args are given, print this help
usage_text = (
"Run blender in background mode with this script:"
" blender --background --python " + __file__ + " -- [options]"
)
parser = argparse.ArgumentParser(description=usage_text)
# Example utility, add some text and renders or saves it (with options)
# Possible types are: string, int, long, choice, float and complex.
parser.add_argument(
"-t", "--text", dest="text", type=str, required=True,
help="This text will be used to render an image",
)
parser.add_argument(
"-s", "--save", dest="save_path", metavar='FILE',
help="Save the generated file to the specified path",
)
parser.add_argument(
"-r", "--render", dest="render_path", metavar='FILE',
help="Render an image to the specified path",
)
args = parser.parse_args(argv) # In this example we won't use the args
if not argv:
parser.print_help()
return
if not args.text:
print("Error: --text=\"some string\" argument not given, aborting.")
parser.print_help()
return
# Run the example function
example_function(args.text, args.save_path, args.render_path)
print("batch job finished, exiting")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
# exports each selected object into its own file
import bpy
import os
# export to blend file location
basedir = os.path.dirname(bpy.data.filepath)
if not basedir:
raise Exception("Blend file is not saved")
view_layer = bpy.context.view_layer
obj_active = view_layer.objects.active
selection = bpy.context.selected_objects
bpy.ops.object.select_all(action='DESELECT')
for obj in selection:
obj.select_set(True)
# some exporters only use the active object
view_layer.objects.active = obj
name = bpy.path.clean_name(obj.name)
fn = os.path.join(basedir, name)
bpy.ops.export_scene.fbx(filepath=fn + ".fbx", use_selection=True)
# Can be used for multiple formats
# bpy.ops.export_scene.x3d(filepath=fn + ".x3d", use_selection=True)
obj.select_set(False)
print("written:", fn)
view_layer.objects.active = obj_active
for obj in selection:
obj.select_set(True)

View File

@@ -0,0 +1,22 @@
# This example assumes we have a mesh object selected
import bpy
import bmesh
# Get the active mesh
me = bpy.context.object.data
# Get a BMesh representation
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
# Modify the BMesh, can do anything here...
for v in bm.verts:
v.co.x += 1.0
# Finish up, write the bmesh back to the mesh
bm.to_mesh(me)
bm.free() # free and prevent further access

View File

@@ -0,0 +1,23 @@
# This example assumes we have a mesh object in edit-mode
import bpy
import bmesh
# Get the active mesh
obj = bpy.context.edit_object
me = obj.data
# Get a BMesh representation
bm = bmesh.from_edit_mesh(me)
bm.faces.active = None
# Modify the BMesh, can do anything here...
for v in bm.verts:
v.co.x += 1.0
# Show the updates in the viewport
# and recalculate n-gon tessellation.
bmesh.update_edit_mesh(me, loop_triangles=True)

View File

@@ -0,0 +1,37 @@
import bpy
class BUILTIN_KSI_hello(bpy.types.KeyingSetInfo):
bl_label = "Hello World KeyingSet"
# poll - test for whether Keying Set can be used at all
def poll(ksi, context):
return context.active_object or context.selected_objects
# iterator - go over all relevant data, calling generate()
def iterator(ksi, context, ks):
for ob in context.selected_objects:
ksi.generate(context, ks, ob)
# generator - populate Keying Set with property paths to use
def generate(ksi, context, ks, data):
id_block = data.id_data
ks.paths.add(id_block, "location")
for i in range(5):
ks.paths.add(id_block, "layers", i, group_method='NAMED', group_name="5x Hello Layers")
ks.paths.add(id_block, "show_in_front", group_method='NONE')
def register():
bpy.utils.register_class(BUILTIN_KSI_hello)
def unregister():
bpy.utils.unregister_class(BUILTIN_KSI_hello)
if __name__ == '__main__':
register()

View File

@@ -0,0 +1,172 @@
import bpy
from bpy.types import NodeTree, Node, NodeSocket, NodeTreeInterfaceSocket
from bl_ui import node_add_menu
# Implementation of custom nodes from Python
# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
# Description string
'''A custom node tree type that will show up in the editor type list'''
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'CustomTreeType'
# Label for nice name display
bl_label = "Custom Node Tree"
# Icon identifier
bl_icon = 'NODETREE'
# Custom socket type
class MyCustomSocket(NodeSocket):
# Description string
"""Custom node socket type"""
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'CustomSocketType'
# Label for nice name display
bl_label = "Custom Node Socket"
input_value: bpy.props.FloatProperty(
name="Value",
description="Value when the socket is not connected",
)
# Optional function for drawing the socket input value
def draw(self, context, layout, node, text):
if self.is_output or self.is_linked:
layout.label(text=text)
else:
layout.prop(self, "input_value", text=text)
# Socket color
@classmethod
def draw_color_simple(cls):
return (1.0, 0.4, 0.216, 0.5)
# Customizable interface properties to generate a socket from.
class MyCustomInterfaceSocket(NodeTreeInterfaceSocket):
# The type of socket that is generated.
bl_socket_idname = 'CustomSocketType'
default_value: bpy.props.FloatProperty(default=1.0, description="Default input value for new sockets",)
def draw(self, context, layout):
# Display properties of the interface.
layout.prop(self, "default_value")
# Set properties of newly created sockets
def init_socket(self, node, socket, data_path):
socket.input_value = self.default_value
# Use an existing socket to initialize the group interface
def from_socket(self, node, socket):
# Current value of the socket becomes the default
self.default_value = socket.input_value
# Mix-in class for all custom nodes in this tree type.
# Defines a poll function to enable instantiation.
class MyCustomTreeNode:
@classmethod
def poll(cls, ntree):
return ntree.bl_idname == 'CustomTreeType'
# Derived from the Node base type.
class MyCustomNode(MyCustomTreeNode, Node):
# === Basics ===
# Description string
'''A custom node'''
# Optional identifier string. If not explicitly defined, the python class name is used.
bl_idname = 'CustomNodeType'
# Label for nice name display
bl_label = "Custom Node"
# Icon identifier
bl_icon = 'SOUND'
# === Custom Properties ===
# These work just like custom properties in ID data blocks
# Extensive information can be found under
# https://docs.blender.org/api/current/bpy.props.html
my_string_prop: bpy.props.StringProperty()
my_float_prop: bpy.props.FloatProperty(default=3.1415926)
# === Optional Functions ===
# Initialization function, called when a new node is created.
# This is the most common place to create the sockets for a node, as shown below.
# NOTE: this is not the same as the standard __init__ function in Python, which is
# a purely internal Python method and unknown to the node system!
def init(self, context):
self.inputs.new('CustomSocketType', "Hello")
self.inputs.new('NodeSocketFloat', "World")
self.inputs.new('NodeSocketVector', "!", use_multi_input=True)
self.outputs.new('NodeSocketColor', "How")
self.outputs.new('NodeSocketColor', "are")
self.outputs.new('NodeSocketFloat', "you")
# Copy function to initialize a copied node from an existing one.
def copy(self, node):
print("Copying from node ", node)
# Free function to clean up on removal.
def free(self):
print("Removing node ", self, ", Goodbye!")
# Additional buttons displayed on the node.
def draw_buttons(self, context, layout):
layout.label(text="Node settings")
layout.prop(self, "my_float_prop")
# Detail buttons in the sidebar.
# If this function is not defined, the draw_buttons function is used instead
def draw_buttons_ext(self, context, layout):
layout.prop(self, "my_float_prop")
# my_string_prop button will only be visible in the sidebar
layout.prop(self, "my_string_prop")
# Optional: custom label
# Explicit user label overrides this, but here we can define a label dynamically
def draw_label(self):
return "I am a custom node"
# Add custom nodes to the Add & Swap menu.
def draw_add_menu(self, context):
layout = self.layout
if context.space_data.tree_type != MyCustomTree.bl_idname:
# Avoid adding nodes to built-in node tree
return
# Add nodes to the layout. Can use submenus, separators, etc. as in any other menu.
self.node_operator(layout, "CustomNodeType")
classes = (
MyCustomTree,
MyCustomSocket,
MyCustomInterfaceSocket,
MyCustomNode,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.NODE_MT_add.append(draw_add_menu)
bpy.types.NODE_MT_swap.append(draw_add_menu)
def unregister():
bpy.types.NODE_MT_add.remove(draw_add_menu)
bpy.types.NODE_MT_swap.remove(draw_add_menu)
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,35 @@
# This script defines functions to be used directly in driver expressions to
# extend the built-in set of python functions.
#
# This can be executed on manually or set to "Register" to
# initialize the functions on file load.
# two sample functions
def invert(f):
""" Simple function call:
invert(val)
"""
return 1.0 - f
uuid_store = {}
def slow_value(value, fac, uuid):
""" Delay the value by a factor, use a unique string to allow
use in multiple drivers without conflict:
slow_value(val, 0.5, "my_value")
"""
value_prev = uuid_store.get(uuid, value)
uuid_store[uuid] = value_new = (value_prev * fac) + (value * (1.0 - fac))
return value_new
import bpy
# Add functions defined in this script into the drivers namespace.
bpy.app.driver_namespace["invert"] = invert
bpy.app.driver_namespace["slow_value"] = slow_value

View File

@@ -0,0 +1,13 @@
# This stub runs a python script relative to the currently open
# blend file, useful when editing scripts externally.
import bpy
import os
# Use your own script name here:
filename = "my_script.py"
filepath = os.path.join(os.path.dirname(bpy.data.filepath), filename)
global_namespace = {"__file__": filepath, "__name__": "__main__"}
with open(filepath, "rb") as file:
exec(compile(file.read(), filepath, 'exec'), global_namespace)

View File

@@ -0,0 +1,35 @@
# This sample shows an efficient way of doing image processing
# over Blender's images using Python.
import bpy
import numpy as np
input_image_name = "Image"
output_image_name = "NewImage"
# Retrieve input image.
input_image = bpy.data.images[input_image_name]
w, h = input_image.size
# Allocate a numpy array to manipulate pixel data.
pixel_data = np.zeros((w, h, 4), 'f')
# Fast copy of pixel data from bpy.data to numpy array.
input_image.pixels.foreach_get(pixel_data.ravel())
# Do whatever image processing you want using numpy here:
# Example 1: Invert red green and blue channels.
pixel_data[:, :, :3] = 1.0 - pixel_data[:, :, :3]
# Example 2: Change gamma on the red channel.
pixel_data[:, :, 0] = np.power(pixel_data[:, :, 0], 1.5)
# Create output image.
if output_image_name in bpy.data.images:
output_image = bpy.data.images[output_image_name]
else:
output_image = bpy.data.images.new(output_image_name, width=w, height=h)
# Copy of pixel data from numpy array back to the output image.
output_image.pixels.foreach_set(pixel_data.ravel())
output_image.update()