# Copyright (C) 2024, Princeton University. # This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. # Authors: Lingjie Mei import bpy import numpy as np from numpy.random import uniform import infinigen from infinigen.assets.material_assignments import AssetList from infinigen.assets.utils.decorate import ( read_co, read_edge_center, read_edge_direction, remove_edges, remove_vertices, select_edges, solidify, subsurf, write_attribute, write_co, ) from infinigen.assets.utils.draw import align_bezier, bezier_curve from infinigen.assets.utils.nodegroup import geo_radius from infinigen.assets.utils.object import join_objects, new_bbox from infinigen.core import surface from infinigen.core.placement.factory import AssetFactory from infinigen.core.surface import NoApply from infinigen.core.util import blender as butil from infinigen.core.util.blender import deep_clone_obj from infinigen.core.util.math import FixedSeed from infinigen.core.util.random import log_uniform from infinigen.core.util.random import random_general as rg class ChairFactory(AssetFactory): back_types = { 0: "whole", 1: "partial", 2: "horizontal-bar", 3: "vertical-bar", } leg_types = { 0: "vertical", 1: "straight", 2: "up-curved", 3: "down-curved", } def __init__(self, factory_seed, coarse=False): super().__init__(factory_seed, coarse) self.get_params_dict() # random init with seed with FixedSeed(self.factory_seed): self.width = uniform(0.4, 0.5) self.size = uniform(0.38, 0.45) self.thickness = uniform(0.04, 0.08) self.bevel_width = self.thickness * (0.1 if uniform() < 0.4 else 0.5) self.seat_back = uniform(0.7, 1.0) if uniform() < 0.75 else 1.0 self.seat_mid = uniform(0.7, 0.8) self.seat_mid_x = uniform( self.seat_back + self.seat_mid * (1 - self.seat_back), 1 ) self.seat_mid_z = uniform(0, 0.5) self.seat_front = uniform(1.0, 1.2) self.is_seat_round = uniform() < 0.6 self.is_seat_subsurf = uniform() < 0.5 self.leg_thickness = uniform(0.04, 0.06) self.limb_profile = uniform(1.5, 2.5) self.leg_height = uniform(0.45, 0.5) self.back_height = uniform(0.4, 0.5) self.is_leg_round = uniform() < 0.5 self.leg_type = np.random.choice( ["vertical", "straight", "up-curved", "down-curved"] ) self.leg_x_offset = 0 self.leg_y_offset = 0, 0 self.back_x_offset = 0 self.back_y_offset = 0 self.has_leg_x_bar = uniform() < 0.6 self.has_leg_y_bar = uniform() < 0.6 self.leg_offset_bar = uniform(0.2, 0.4), uniform(0.6, 0.8) self.has_arm = uniform() < 0.7 self.arm_thickness = uniform(0.04, 0.06) self.arm_height = self.arm_thickness * uniform(0.6, 1) self.arm_y = uniform(0.8, 1) * self.size self.arm_z = uniform(0.3, 0.6) * self.back_height self.arm_mid = np.array( [uniform(-0.03, 0.03), uniform(-0.03, 0.09), uniform(-0.09, 0.03)] ) self.arm_profile = log_uniform(0.1, 3, 2) self.back_thickness = uniform(0.04, 0.05) self.back_type = rg(self.back_types) self.back_profile = [(0, 1)] self.back_vertical_cuts = np.random.randint(1, 4) self.back_partial_scale = uniform(1, 1.4) materials = AssetList["ChairFactory"]() self.limb_surface = materials["limb"].assign_material() self.surface = materials["surface"].assign_material() if uniform() < 0.3: self.panel_surface = self.surface else: self.panel_surface = materials["panel"].assign_material() scratch_prob, edge_wear_prob = materials["wear_tear_prob"] self.scratch, self.edge_wear = materials["wear_tear"] is_scratch = uniform() < scratch_prob is_edge_wear = uniform() < edge_wear_prob if not is_scratch: self.scratch = None if not is_edge_wear: self.edge_wear = None # from infinigen.assets.clothes import blanket # from infinigen.assets.scatters.clothes import ClothesCover # self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(.8, 1.2), # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply() self.clothes_scatter = NoApply() self.post_init() def get_params_dict(self): # all the parameters (key:name, value: [type, range]) used in this generator self.params_dict = { "width": ['continuous', [0.3, 0.8]], # seat width "size": ['continuous', [0.35, 0.5]], # seat length "thickness": ['continuous', [0.02, 0.1]], # seat thickness "bevel_width": ['discrete', [0.1, 0.5]], "seat_back": ['continuous', [0.6, 1.0]], # seat back width "seat_mid": ['continuous', [0.7, 0.8]], "seat_mid_z": ['continuous', [0.0, 0.7]], # seat mid point height "seat_front": ['continuous', [1.0, 1.2]], # seat front point "is_seat_round": ['discrete', [0, 1]], "is_seat_subsurf": ['discrete', [0, 1]], "leg_thickness": ['continuous', [0.02, 0.07]], # leg thickness "limb_profile": ['continuous', [1.5, 2.5]], "leg_height": ['continuous', [0.2, 1.0]], # leg height "is_leg_round": ['discrete', [0, 1]], "leg_type": ['discrete', [0,1,2,3]], "has_leg_x_bar": ['discrete', [0, 1]], "has_leg_y_bar": ['discrete', [0, 1]], "leg_offset_bar0": ['continuous', [0.1, 0.9]], # leg y bar offset, only for has_leg_y_bar is 1 "leg_offset_bar1": ['continuous', [0.1, 0.9]], # leg x bar offset, only for has_leg_x_bar is 1 "leg_x_offset": ['continuous', [0.0, 0.2]], # leg end point x offset "leg_y_offset0": ['continuous', [0.0, 0.2]], # leg end point y offset "leg_y_offset1": ['continuous', [0.0, 0.2]], # leg end point y offset "has_arm": ['discrete', [0, 1]], "arm_thickness": ['continuous', [0.02, 0.07]], # arm thickness, only for has_arm is 1 "arm_height": ['continuous', [0.6, 1]], # only for has_arm is 1 "arm_y": ['continuous', [0.5, 1]], # arm y end point, only for has_arm is 1 "arm_z": ['continuous', [0.25, 0.6]], # arm z end point, only for has_arm is 1 "arm_mid0": ['continuous', [-0.03, 0.03]], # arm mid point x coord, only for has_arm is 1 "arm_mid1": ['continuous', [-0.03, 0.2]], # arm mid point y coord, only for has_arm is 1 "arm_mid2": ['continuous', [-0.09, 0.03]], # arm mid point z coord, only for has_arm is 1 "arm_profile0": ['continuous', [0.0, 2.0]], # arm curve control, only for has_arm is 1 "arm_profile1": ['continuous', [0.0, 2]], # arm curve control, only for has_arm is 1 "back_height": ['continuous', [0.3, 0.6]], # back height "back_thickness": ['continuous', [0.02, 0.07]], # back thickness "back_type": ['discrete', [0, 1, 2, 3]], "back_vertical_cuts": ['discrete', [1,2,3,4]], # only for back type 3 "back_partial_scale": ['continuous', [1.0, 1.4]], # only for back type 1 "back_x_offset": ['continuous', [-0.1, 0.15]], # back top x length "back_y_offset": ['continuous', [0.0, 0.4]], # back top y coord "back_profile_partial": ['continuous', [0.4, 0.8]], # only for back type 1 "back_profile_horizontal_ncuts": ['discrete', [2, 3, 4]], # only for back type 2 "back_profile_horizontal_locs0": ['continuous', [1, 2]], # only for back type 2 "back_profile_horizontal_locs1": ['continuous', [1, 2]], # only for back type 2 "back_profile_horizontal_locs2": ['continuous', [1, 2]], # only for back type 2 "back_profile_horizontal_locs3": ['continuous', [1, 2]], # only for back type 2 "back_profile_horizontal_ratio": ['continuous', [0.2, 0.8]], # only for back type 2 "back_profile_horizontal_lowest": ['continuous', [0, 0.4]], # only for back type 2 "back_profile_vertical": ['continuous', [0.8, 0.9]], # only for back type 3 } def fix_unused_params(self, params): # check unused parameters inside a given parameter set, and fix them into mid value - for training if params['leg_type'] != 2 and params['leg_type'] != 3: params['limb_profile'] = (self.params_dict['limb_profile'][1][0] + self.params_dict['limb_profile'][1][-1]) / 2 if params['has_leg_x_bar'] == 0: params['leg_offset_bar1'] = (self.params_dict['leg_offset_bar1'][1][0] + self.params_dict['leg_offset_bar1'][1][-1]) / 2 if params['has_leg_y_bar'] == 0: params['leg_offset_bar0'] = (self.params_dict['leg_offset_bar0'][1][0] + self.params_dict['leg_offset_bar0'][1][-1]) / 2 if params['has_arm'] == 0: params['arm_thickness'] = (self.params_dict['arm_thickness'][1][0] + self.params_dict['arm_thickness'][1][-1]) / 2 params['arm_height'] = (self.params_dict['arm_height'][1][0] + self.params_dict['arm_height'][1][-1]) / 2 params['arm_y'] = (self.params_dict['arm_y'][1][0] + self.params_dict['arm_y'][1][-1]) / 2 params['arm_z'] = (self.params_dict['arm_z'][1][0] + self.params_dict['arm_z'][1][-1]) / 2 params['arm_mid0'] = (self.params_dict['arm_mid0'][1][0] + self.params_dict['arm_mid0'][1][-1]) / 2 params['arm_mid1'] = (self.params_dict['arm_mid1'][1][0] + self.params_dict['arm_mid1'][1][-1]) / 2 params['arm_mid2'] = (self.params_dict['arm_mid2'][1][0] + self.params_dict['arm_mid2'][1][-1]) / 2 params['arm_profile0'] = (self.params_dict['arm_profile0'][1][0] + self.params_dict['arm_profile0'][1][-1]) / 2 params['arm_profile1'] = (self.params_dict['arm_profile1'][1][0] + self.params_dict['arm_profile1'][1][-1]) / 2 if params['back_type'] != 3: params['back_vertical_cuts'] = (self.params_dict['back_vertical_cuts'][1][0] + self.params_dict['back_vertical_cuts'][1][-1]) / 2 params['back_profile_vertical'] = (self.params_dict['back_profile_vertical'][1][0] + self.params_dict['back_profile_vertical'][1][-1]) / 2 if params['back_type'] != 2: params['back_profile_horizontal_ncuts'] = (self.params_dict['back_profile_horizontal_ncuts'][1][0] + self.params_dict['back_profile_horizontal_ncuts'][1][-1]) / 2 params['back_profile_horizontal_locs0'] = (self.params_dict['back_profile_horizontal_locs0'][1][0] + self.params_dict['back_profile_horizontal_locs0'][1][-1]) / 2 params['back_profile_horizontal_locs1'] = (self.params_dict['back_profile_horizontal_locs1'][1][0] + self.params_dict['back_profile_horizontal_locs1'][1][-1]) / 2 params['back_profile_horizontal_locs2'] = (self.params_dict['back_profile_horizontal_locs2'][1][0] + self.params_dict['back_profile_horizontal_locs2'][1][-1]) / 2 params['back_profile_horizontal_ratio'] = (self.params_dict['back_profile_horizontal_ratio'][1][0] + self.params_dict['back_profile_horizontal_ratio'][1][-1]) / 2 params['back_profile_horizontal_lowest'] = (self.params_dict['back_profile_horizontal_lowest'][1][0] + self.params_dict['back_profile_horizontal_lowest'][1][-1]) / 2 if params['back_type'] != 1: params['back_partial_scale'] = (self.params_dict['back_partial_scale'][1][0] + self.params_dict['back_partial_scale'][1][-1]) / 2 params['back_profile_partial'] = (self.params_dict['back_profile_partial'][1][0] + self.params_dict['back_profile_partial'][1][-1]) / 2 return params def update_params(self, new_params): # replace the parameters and calculate all the new values self.width = new_params["width"] self.size = new_params["size"] self.thickness = new_params["thickness"] self.bevel_width = self.thickness * new_params["bevel_width"] self.seat_back = new_params["seat_back"] self.seat_mid = new_params["seat_mid"] self.seat_mid_x = uniform( self.seat_back + self.seat_mid * (1 - self.seat_back), 1 ) self.seat_mid_z = new_params["seat_mid_z"] self.seat_front = new_params["seat_front"] self.is_seat_round = new_params["is_seat_round"] self.is_seat_subsurf = new_params["is_seat_subsurf"] self.leg_thickness = new_params["leg_thickness"] self.limb_profile = new_params["limb_profile"] self.leg_height = new_params["leg_height"] self.back_height = new_params["back_height"] self.is_leg_round = new_params["is_leg_round"] self.leg_type = self.leg_types[new_params["leg_type"]] self.leg_x_offset = 0 self.leg_y_offset = 0, 0 self.back_x_offset = 0 self.back_y_offset = 0 self.has_leg_x_bar = new_params["has_leg_x_bar"] self.has_leg_y_bar = new_params["has_leg_y_bar"] self.leg_offset_bar = new_params["leg_offset_bar0"], new_params["leg_offset_bar1"] self.has_arm = new_params["has_arm"] self.arm_thickness = new_params["arm_thickness"] self.arm_height = self.arm_thickness * new_params["arm_height"] self.arm_y = new_params["arm_y"] * self.size self.arm_z = new_params["arm_z"] * self.back_height self.arm_mid = np.array( [new_params["arm_mid0"], new_params["arm_mid1"], new_params["arm_mid2"]] ) self.arm_profile = (new_params["arm_profile0"], new_params["arm_profile1"]) self.back_thickness = new_params["back_thickness"] self.back_type = self.back_types[new_params["back_type"]] self.back_profile = [(0, 1)] self.back_vertical_cuts = new_params["back_vertical_cuts"] self.back_partial_scale = new_params["back_partial_scale"] if self.leg_type == "vertical": self.leg_x_offset = 0 self.leg_y_offset = 0, 0 self.back_x_offset = 0 self.back_y_offset = 0 else: self.leg_x_offset = self.width * new_params["leg_x_offset"] self.leg_y_offset = self.size * np.array([new_params["leg_y_offset0"], new_params["leg_y_offset1"]]) self.back_x_offset = self.width * new_params["back_x_offset"] self.back_y_offset = self.size * new_params["back_y_offset"] match self.back_type: case "partial": self.back_profile = ((new_params["back_profile_partial"], 1),) case "horizontal-bar": n_cuts = int(new_params["back_profile_horizontal_ncuts"]) locs = np.array([new_params["back_profile_horizontal_locs0"], new_params["back_profile_horizontal_locs1"], new_params["back_profile_horizontal_locs2"], new_params["back_profile_horizontal_locs3"]])[:n_cuts].cumsum() locs = locs / locs[-1] ratio = new_params["back_profile_horizontal_ratio"] locs = np.array( [ (p + ratio * (l - p), l) for p, l in zip([0, *locs[:-1]], locs) ] ) lowest = new_params["back_profile_horizontal_lowest"] self.back_profile = locs * (1 - lowest) + lowest case "vertical-bar": self.back_profile = ((new_params["back_profile_vertical"], 1),) case _: self.back_profile = [(0, 1)] # TODO: handle the material into the optimization loop materials = AssetList["ChairFactory"]() self.limb_surface = materials["limb"].assign_material() self.surface = materials["surface"].assign_material() if uniform() < 0.3: self.panel_surface = self.surface else: self.panel_surface = materials["panel"].assign_material() scratch_prob, edge_wear_prob = materials["wear_tear_prob"] self.scratch, self.edge_wear = materials["wear_tear"] is_scratch = uniform() < scratch_prob is_edge_wear = uniform() < edge_wear_prob if not is_scratch: self.scratch = None if not is_edge_wear: self.edge_wear = None # from infinigen.assets.clothes import blanket # from infinigen.assets.scatters.clothes import ClothesCover # self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(.8, 1.2), # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply() self.clothes_scatter = NoApply() def post_init(self): with FixedSeed(self.factory_seed): if self.leg_type == "vertical": self.leg_x_offset = 0 self.leg_y_offset = 0, 0 self.back_x_offset = 0 self.back_y_offset = 0 else: self.leg_x_offset = self.width * uniform(0.05, 0.2) self.leg_y_offset = self.size * uniform(0.05, 0.2, 2) self.back_x_offset = self.width * uniform(-0.1, 0.15) self.back_y_offset = self.size * uniform(0.1, 0.25) match self.back_type: case "partial": self.back_profile = ((uniform(0.4, 0.8), 1),) case "horizontal-bar": n_cuts = np.random.randint(2, 4) locs = uniform(1, 2, n_cuts).cumsum() locs = locs / locs[-1] ratio = uniform(0.5, 0.75) locs = np.array( [ (p + ratio * (l - p), l) for p, l in zip([0, *locs[:-1]], locs) ] ) lowest = uniform(0, 0.4) self.back_profile = locs * (1 - lowest) + lowest case "vertical-bar": self.back_profile = ((uniform(0.8, 0.9), 1),) case _: self.back_profile = [(0, 1)] def create_placeholder(self, **kwargs) -> bpy.types.Object: obj = new_bbox( -self.width / 2 - max(self.leg_x_offset, self.back_x_offset), self.width / 2 + max(self.leg_x_offset, self.back_x_offset), -self.size - self.leg_y_offset[1] - self.leg_thickness * 0.5, max(self.leg_y_offset[0], self.back_y_offset), -self.leg_height, self.back_height * 1.2, ) obj.rotation_euler.z += np.pi / 2 butil.apply_transform(obj) return obj def create_asset(self, **params) -> bpy.types.Object: obj = self.make_seat() legs = self.make_legs() backs = self.make_backs() parts = [obj] + legs + backs parts.extend(self.make_leg_decors(legs)) if self.has_arm: parts.extend(self.make_arms(obj, backs)) parts.extend(self.make_back_decors(backs)) for obj in legs: self.solidify(obj, 2) for obj in backs: self.solidify(obj, 2, self.back_thickness) obj = join_objects(parts) obj.rotation_euler.z += np.pi / 2 butil.apply_transform(obj) with FixedSeed(self.factory_seed): # TODO: wasteful to create unique materials for each individual asset self.surface.apply(obj) self.panel_surface.apply(obj, selection="panel") self.limb_surface.apply(obj, selection="limb") return obj def finalize_assets(self, assets): if self.scratch: self.scratch.apply(assets) if self.edge_wear: self.edge_wear.apply(assets) def make_seat(self): x_anchors = ( np.array( [ 0, -self.seat_back, -self.seat_mid_x, -1, 0, 1, self.seat_mid_x, self.seat_back, 0, ] ) * self.width / 2 ) y_anchors = ( np.array( [0, 0, -self.seat_mid, -1, -self.seat_front, -1, -self.seat_mid, 0, 0] ) * self.size ) z_anchors = ( np.array([0, 0, self.seat_mid_z, 0, 0, 0, self.seat_mid_z, 0, 0]) * self.thickness ) vector_locations = [1, 7] if self.is_seat_round else [1, 3, 5, 7] obj = bezier_curve((x_anchors, y_anchors, z_anchors), vector_locations, 8) with butil.ViewportMode(obj, "EDIT"): bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.fill_grid(use_interp_simple=True) butil.modify_mesh(obj, "SOLIDIFY", thickness=self.thickness, offset=0) subsurf(obj, 1, not self.is_seat_subsurf) butil.modify_mesh(obj, "BEVEL", width=self.bevel_width, segments=8) return obj def make_legs(self): leg_starts = np.array( [[-self.seat_back, 0, 0], [-1, -1, 0], [1, -1, 0], [self.seat_back, 0, 0]] ) * np.array([[self.width / 2, self.size, 0]]) leg_ends = leg_starts.copy() leg_ends[[0, 1], 0] -= self.leg_x_offset leg_ends[[2, 3], 0] += self.leg_x_offset leg_ends[[0, 3], 1] += self.leg_y_offset[0] leg_ends[[1, 2], 1] -= self.leg_y_offset[1] leg_ends[:, -1] = -self.leg_height return self.make_limb(leg_ends, leg_starts) def make_limb(self, leg_ends, leg_starts): limbs = [] for leg_start, leg_end in zip(leg_starts, leg_ends): match self.leg_type: case "up-curved": axes = [(0, 0, 1), None] scale = [self.limb_profile, 1] case "down-curved": axes = [None, (0, 0, 1)] scale = [1, self.limb_profile] case _: axes = None scale = None limb = align_bezier( np.stack([leg_start, leg_end], -1), axes, scale, resolution=64 ) limb.location = ( np.array( [ 1 if leg_start[0] < 0 else -1, 1 if leg_start[1] < -self.size / 2 else -1, 0, ] ) * self.leg_thickness / 2 ) butil.apply_transform(limb, True) limbs.append(limb) return limbs def make_backs(self): back_starts = ( np.array([[-self.seat_back, 0, 0], [self.seat_back, 0, 0]]) * self.width / 2 ) back_ends = back_starts.copy() back_ends[:, 0] += np.array([self.back_x_offset, -self.back_x_offset]) back_ends[:, 1] = self.back_y_offset back_ends[:, 2] = self.back_height return self.make_limb(back_starts, back_ends) def make_leg_decors(self, legs): decors = [] if self.has_leg_x_bar: z_height = -self.leg_height * uniform(*self.leg_offset_bar) locs = [] for leg in legs: co = read_co(leg) locs.append(co[np.argmin(np.abs(co[:, -1] - z_height))]) decors.append( self.solidify(bezier_curve(np.stack([locs[0], locs[3]], -1)), 0) ) decors.append( self.solidify(bezier_curve(np.stack([locs[1], locs[2]], -1)), 0) ) if self.has_leg_y_bar: z_height = -self.leg_height * uniform(*self.leg_offset_bar) locs = [] for leg in legs: co = read_co(leg) locs.append(co[np.argmin(np.abs(co[:, -1] - z_height))]) decors.append( self.solidify(bezier_curve(np.stack([locs[0], locs[1]], -1)), 1) ) decors.append( self.solidify(bezier_curve(np.stack([locs[2], locs[3]], -1)), 1) ) for d in decors: write_attribute(d, 1, "limb", "FACE") return decors def make_back_decors(self, backs, finalize=True): obj = join_objects([deep_clone_obj(b) for b in backs]) x, y, z = read_co(obj).T x += np.where(x > 0, self.back_thickness / 2, -self.back_thickness / 2) write_co(obj, np.stack([x, y, z], -1)) smoothness = uniform(0, 1) profile_shape_factor = uniform(0, 0.4) with butil.ViewportMode(obj, "EDIT"): bpy.ops.mesh.select_mode(type="EDGE") center = read_edge_center(obj) for z_min, z_max in self.back_profile: select_edges( obj, (z_min * self.back_height <= center[:, -1]) & (center[:, -1] <= z_max * self.back_height), ) bpy.ops.mesh.bridge_edge_loops( number_cuts=32, interpolation="LINEAR", smoothness=smoothness, profile_shape_factor=profile_shape_factor, ) bpy.ops.mesh.select_loose() bpy.ops.mesh.delete() butil.modify_mesh( obj, "SOLIDIFY", thickness=np.minimum(self.thickness, self.back_thickness), offset=0, ) if finalize: butil.modify_mesh(obj, "BEVEL", width=self.bevel_width, segments=8) parts = [obj] if self.back_type == "vertical-bar": other = join_objects([deep_clone_obj(b) for b in backs]) with butil.ViewportMode(other, "EDIT"): bpy.ops.mesh.select_mode(type="EDGE") bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.bridge_edge_loops( number_cuts=self.back_vertical_cuts, interpolation="LINEAR", smoothness=smoothness, profile_shape_factor=profile_shape_factor, ) bpy.ops.mesh.select_all(action="INVERT") bpy.ops.mesh.delete() bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.delete(type="ONLY_FACE") remove_edges(other, np.abs(read_edge_direction(other)[:, -1]) < 0.5) remove_vertices(other, lambda x, y, z: z < -self.thickness / 2) remove_vertices( other, lambda x, y, z: z > (self.back_profile[0][0] + self.back_profile[0][1]) * self.back_height / 2, ) parts.append(self.solidify(other, 2, self.back_thickness)) elif self.back_type == "partial": co = read_co(obj) co[:, 1] *= self.back_partial_scale write_co(obj, co) for p in parts: write_attribute(p, 1, "panel", "FACE") return parts def make_arms(self, base, backs): co = read_co(base) end = co[np.argmin(co[:, 0] - (np.abs(co[:, 1] + self.arm_y) < 0.02))] end[0] += self.arm_thickness / 4 end_ = end.copy() end_[0] = -end[0] arms = [] co = read_co(backs[0]) start = co[np.argmin(co[:, 0] - (np.abs(co[:, -1] - self.arm_z) < 0.02))] start[0] -= self.arm_thickness / 4 start_ = start.copy() start_[0] = -start[0] for start, end in zip([start, start_], [end, end_]): mid = np.array( [ end[0] + self.arm_mid[0] * (-1 if end[0] > 0 else 1), end[1] + self.arm_mid[1], start[2] + self.arm_mid[2], ] ) arm = align_bezier( np.stack([start, mid, end], -1), np.array( [ [end[0] - start[0], end[1] - start[1], 0], [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], [0, 0, 1], ] ), [1, *self.arm_profile, 1], ) if self.is_leg_round: surface.add_geomod( arm, geo_radius, apply=True, input_args=[self.arm_thickness / 2, 32], input_kwargs={"to_align_tilt": False}, ) else: with butil.ViewportMode(arm, "EDIT"): bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.extrude_edges_move( TRANSFORM_OT_translate={ "value": ( self.arm_thickness if end[0] < 0 else -self.arm_thickness, 0, 0, ) } ) butil.modify_mesh(arm, "SOLIDIFY", thickness=self.arm_height, offset=0) write_attribute(arm, 1, "limb", "FACE") arms.append(arm) return arms def solidify(self, obj, axis, thickness=None): if thickness is None: thickness = self.leg_thickness if self.is_leg_round: solidify(obj, axis, thickness) butil.modify_mesh(obj, "BEVEL", width=self.bevel_width, segments=8) else: surface.add_geomod( obj, geo_radius, apply=True, input_args=[thickness / 2, 32] ) write_attribute(obj, 1, "limb", "FACE") return obj