OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / utils / profile.py

140 lines · python ref:
import bpy
import pathlib
import json
from ctypes import Structure, c_float, c_short, c_char, c_ubyte, cast, POINTER, memmove, sizeof


def get_bevel_profile(mod: bpy.types.BevelModifier) -> dict:
    profile = mod.custom_profile
    points = profile.points

    data = {
        'segments': mod.segments,
        'profile': {
            'use_sample_straight_edges':  profile.use_sample_straight_edges,
            'use_sample_even_lengths': profile.use_sample_even_lengths,
            'points': [],
            'native_points': [],
        },
    }

    for point in points:
        data['profile']['points'].append({
            'handle_type_1': point.handle_type_1,
            'handle_type_2': point.handle_type_2,
            'location': point.location[:],
        })

        native_point = cast(point.as_pointer(), POINTER(c_ubyte * sizeof(CurveProfilePoint))).contents
        data['profile']['native_points'].append(list(native_point))

    return data


def save_bevel_profile(mod: bpy.types.BevelModifier, path: str) -> tuple:
    if getattr(mod, 'use_custom_profile', True) == False:
        return {'WARNING'}, f'Modifier "{mod.name}" is not using a custom profile', {'CANCELLED'}
    elif getattr(mod, 'profile_type', 'CUSTOM') != 'CUSTOM':
        return {'WARNING'}, f'Modifier "{mod.name}" is not using a custom profile', {'CANCELLED'}

    data = get_bevel_profile(mod)
    data = json.dumps(data, indent=4)

    path = bpy.path.abspath(path)
    path = pathlib.Path(path).resolve()

    try:
        size = path.write_text(data)
    except:
        size = None

    if size:
        return {'INFO'}, f'Saved "{path.name}"', {'FINISHED'}
    else:
        return {'ERROR'}, f'Failed to save "{path.name}"', {'CANCELLED'}


def set_bevel_profile(mod: bpy.types.BevelModifier, data: dict, sync_segments: bool) -> bool:
    if hasattr(mod, 'use_custom_profile'):
        mod.use_custom_profile = True
    elif hasattr(mod, 'profile_type'):
        mod.profile_type = 'CUSTOM'

    profile = mod.custom_profile
    points = profile.points

    mod.segments = len(data['profile']['points']) if sync_segments else data['segments']
    profile.use_sample_straight_edges = data['profile']['use_sample_straight_edges']
    profile.use_sample_even_lengths = data['profile']['use_sample_even_lengths']

    handle_type_1 = points[0].bl_rna.properties['handle_type_1']
    handle_types = [v.identifier for v in handle_type_1.enum_items]

    while len(points) > 2:
        points.remove(points[1])

    points[0].location = (0.0, 0.5)
    points[-1].location = (1.0, 0.5)
    total = len(data['profile']['points']) - 1

    for index, point_data in enumerate(data['profile']['points'][1:-1], start=1):
        point = points.add(index / total, 0.5)

        handle_type_1 = point_data['handle_type_1']
        if handle_type_1 in handle_types:
            point.handle_type_1 = handle_type_1
        else:
            print(f'{handle_type_1} is not supported')
            point.handle_type_1 = 'AUTO'

        handle_type_2 = point_data['handle_type_2']
        if handle_type_2 in handle_types:
            point.handle_type_2 = handle_type_2
        else:
            print(f'{handle_type_2} is not supported')
            point.handle_type_2 = 'AUTO'

    for index, point_data in enumerate(data['profile']['points']):
        location = point_data['location']
        points[index].location = location

    try:
        for bpoint, point in zip(data['profile']['native_points'], points):
            bpoint = bytes(bpoint)
            native_point_ptr = cast(point.as_pointer(), POINTER(CurveProfilePoint))
            memmove(native_point_ptr, bpoint, len(bpoint))
    except KeyError:
        pass

    profile.update()
    return True


def load_bevel_profile(mod: bpy.types.BevelModifier, path: str, sync_segments: bool) -> tuple:
    path = bpy.path.abspath(path)
    path = pathlib.Path(path).resolve()

    try:
        data = path.read_text()
    except:
        data = None

    if data:
        data = json.loads(data)
    else:
        return {'ERROR'}, f'Failed to load "{path.name}"', {'CANCELLED'}

    if set_bevel_profile(mod, data, sync_segments):
        return {'INFO'}, f'Loaded "{path.name}"', {'FINISHED'}


class CurveProfilePoint(Structure):
    _fields_ = [
        ('x', c_float),
        ('y', c_float),
        ('flag', c_short),
        ('h1', c_char),
        ('h2', c_char),
        ('h1_loc', c_float * 2),
        ('h2_loc', c_float * 2)]