diff --git a/bbp_ng/OP_ADDS_rail.py b/bbp_ng/OP_ADDS_rail.py index e6a3d98..72407f2 100644 --- a/bbp_ng/OP_ADDS_rail.py +++ b/bbp_ng/OP_ADDS_rail.py @@ -50,9 +50,15 @@ class SharedRailSectionInputProperty(): @param force_monorail[in] Force this draw method for monorail if True, or for rail if False. Accept None if you want user to choose it. """ + # draw title + layout = layout.box() + layout.label(text = 'Section') + if force_monorail is None: # show picker to allow user pick - layout.prop(self, 'rail_type', expand = True) + # force it show horizontal + row = layout.row() + row.prop(self, 'rail_type', expand = True) # show radius layout.prop(self, "rail_radius") # show span for rail @@ -92,6 +98,9 @@ class SharedRailCapInputProperty(): ) # type: ignore def draw_rail_cap_input(self, layout: bpy.types.UILayout) -> None: + # draw title + layout = layout.box() + layout.label(text = 'Cap') row = layout.row() row.prop(self, "rail_start_cap", toggle = 1) row.prop(self, "rail_end_cap", toggle = 1) @@ -116,6 +125,9 @@ class SharedStraightRailInputProperty(): ) # type: ignore def draw_straight_rail_input(self, layout: bpy.types.UILayout) -> None: + # draw title + layout = layout.box() + layout.label(text = 'Straight Rail') layout.prop(self, "rail_length") def general_get_rail_length(self) -> float: @@ -162,6 +174,10 @@ class SharedScrewRailInputProperty(): ) # type: ignore def draw_screw_rail_input(self, layout: bpy.types.UILayout, show_for_screw: bool) -> None: + # draw title + layout = layout.box() + layout.label(text = 'Screw Rail') + if show_for_screw: # screw do not need angle property layout.prop(self, "rail_screw_screw") @@ -374,6 +390,11 @@ def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], No mesh: bpy.types.Mesh = bpy.data.meshes.new('Rail') bm.to_mesh(mesh) bm.free() + + # setup smooth for mesh + mesh.use_auto_smooth = True + mesh.auto_smooth_angle = math.radians(50) + mesh.shade_smooth() # create object and assoc with it # create info first diff --git a/bbp_ng/UTIL_bme.py b/bbp_ng/UTIL_bme.py index 664c91f..13155ef 100644 --- a/bbp_ng/UTIL_bme.py +++ b/bbp_ng/UTIL_bme.py @@ -92,7 +92,7 @@ _g_ProgFieldGlobals: dict[str, typing.Any] = { # constant 'pi': math.pi, 'tau': math.tau, - + # math functions 'sin': math.sin, 'cos': math.cos, @@ -100,15 +100,15 @@ _g_ProgFieldGlobals: dict[str, typing.Any] = { 'asin': math.asin, 'acos': math.acos, 'atan': math.atan, - + 'pow': math.pow, 'sqrt': math.sqrt, - + 'fabs': math.fabs, - + 'degrees': math.degrees, 'radians': math.radians, - + # builtin functions 'abs': abs, @@ -116,7 +116,7 @@ _g_ProgFieldGlobals: dict[str, typing.Any] = { 'float': float, 'str': str, 'bool': bool, - + # my custom matrix functions 'move': lambda x, y, z: mathutils.Matrix.Translation((x, y, z)), 'rot': lambda x, y, z: mathutils.Matrix.LocRotScale(None, mathutils.Euler((math.radians(x), math.radians(y), math.radians(z)), 'XYZ'), None), @@ -144,22 +144,22 @@ def _eval_others(strl: str, params_vars_data: dict[str, typing.Any]) -> typing.A class PrototypeShowcaseCfgDescriptor(): __mRawCfg: dict[str, str] - + def __init__(self, raw_cfg: dict[str, str]): self.__mRawCfg = raw_cfg - - def get_field(self) -> str: + + def get_field(self) -> str: return self.__mRawCfg[TOKEN_SHOWCASE_CFGS_FIELD] - + def get_type(self) -> PrototypeShowcaseCfgsTypes: return PrototypeShowcaseCfgsTypes(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_TYPE]) - - def get_title(self) -> str: + + def get_title(self) -> str: return self.__mRawCfg[TOKEN_SHOWCASE_CFGS_TITLE] - - def get_desc(self) -> str: + + def get_desc(self) -> str: return self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DESC] - + def get_default(self) -> typing.Any: return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT]) @@ -167,7 +167,7 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper): """ The BME specialized Blender EnumProperty helper. """ - + def __init__(self): # init parent class UTIL_functions.EnumPropHelper.__init__( @@ -179,7 +179,7 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper): lambda _: '', lambda x: self.get_bme_showcase_icon(x) ) - + def get_bme_identifiers(self) -> tuple[str, ...]: """ Get the identifier of prototype which need to be exposed to user. @@ -198,7 +198,7 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper): proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) # visit title field return proto[TOKEN_SHOWCASE][TOKEN_SHOWCASE_TITLE] - + def get_bme_showcase_icon(self, ident: str) -> int: """ Get BME icon by prototype's identifier @@ -210,7 +210,7 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper): cache: int | None = UTIL_icons_manager.get_bme_icon(icon_name) if cache is None: return UTIL_icons_manager.get_empty_icon() else: return cache - + def get_bme_showcase_cfgs(self, ident: str) -> typing.Iterator[PrototypeShowcaseCfgDescriptor]: # get prototype first proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) @@ -224,15 +224,15 @@ class EnumPropHelper(UTIL_functions.EnumPropHelper): def create_bme_struct_wrapper(ident: str, cfgs: dict[str, typing.Any]) -> bpy.types.Object: # get prototype first proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) - + # analyse params by given cfgs params: dict[str, typing.Any] = {} for proto_param in proto[TOKEN_PARAMS]: params[proto_param[TOKEN_PARAMS_FIELD]] = _eval_params(proto_param[TOKEN_PARAMS_DATA], cfgs) - + # create used mesh mesh: bpy.types.Mesh = bpy.data.meshes.new('BMEStruct') - + # create mesh writer and bme mtl helper # recursively calling underlying creation function with UTIL_blender_mesh.MeshWriter(mesh) as writer: @@ -244,7 +244,7 @@ def create_bme_struct_wrapper(ident: str, cfgs: dict[str, typing.Any]) -> bpy.ty mathutils.Matrix.Identity(4), params ) - + # create object and assign prop # get obj info first obj_info: UTIL_naming_convension.BallanceObjectInfo @@ -264,23 +264,23 @@ def create_bme_struct_wrapper(ident: str, cfgs: dict[str, typing.Any]) -> bpy.ty obj: bpy.types.Object = bpy.data.objects.new(obj_name, mesh) # assign virtools groups UTIL_naming_convension.VirtoolsGroupConvention.set_to_object(obj, obj_info, None) - + # return object return obj def create_bme_struct( - ident: str, - writer: UTIL_blender_mesh.MeshWriter, + ident: str, + writer: UTIL_blender_mesh.MeshWriter, bmemtl: PROP_bme_material.BMEMaterialsHelper, - transform: mathutils.Matrix, + transform: mathutils.Matrix, params: dict[str, typing.Any]) -> None: # get prototype first proto: dict[str, typing.Any] = _get_prototype_by_identifier(ident) - + # check whether skip the whole struct before cal vars if _eval_skip(proto[TOKEN_SKIP], params) == True: return - + # calc vars by given params # please note i will add entries directly into params dict # but the params dict will not used independently later, @@ -288,7 +288,7 @@ def create_bme_struct( # so it is safe. for proto_var in proto[TOKEN_VARS]: params[proto_var[TOKEN_VARS_FIELD]] = _eval_vars(proto_var[TOKEN_VARS_DATA], params) - + # collect valid face and vertices data for following using. # if NOT skip, add into valid list valid_vec_idx: list[int] = [] @@ -299,36 +299,65 @@ def create_bme_struct( for face_idx, proto_face in enumerate(proto[TOKEN_FACES]): if _eval_others(proto_face[TOKEN_FACES_SKIP], params) == False: valid_face_idx.append(face_idx) - + # create mtl slot remap to help following mesh adding # because mesh writer do not accept string format mtl slot visiting, # it only accept int based mtl slot index. + # + # Also we build face used mtl slot index at the same time. + # So we do not analyse texture field again when providing face data. + # The result is in `prebuild_face_mtl_idx` and please note it will store all face's mtl index. + # For example: if face 0 is skipped and face 1 is used, the first entry in `prebuild_face_mtl_idx` + # will be the mtl slot index used by face 0, not 1. And its length is equal to the face count. + # However, because face 0 is skipped, so the entry is not used and default set to 0. + # # NOTE: since Python 3.6, the item of builtin dict is ordered by inserting order. - # we rely on this to implement following features + # we rely on this to implement following features. mtl_remap: dict[str, int] = {} + prebuild_face_mtl_idx: list[int] = [0] * len(proto[TOKEN_FACES]) for face_idx in valid_face_idx: # eval mtl name mtl_name: str = _eval_others(proto[TOKEN_FACES][face_idx][TOKEN_FACES_TEXTURE], params) - # add into remap if not exist + # try insert into remap and record to face mtl idx if mtl_name not in mtl_remap: + # record index + prebuild_face_mtl_idx[face_idx] = len(mtl_remap) + # add into remap if not exist mtl_remap[mtl_name] = len(mtl_remap) + else: + # if existing, no need to add into remap + # but we need get its index from remap + prebuild_face_mtl_idx[face_idx] = mtl_remap.get(mtl_name, 0) + # pre-compute vertices data because we may need used later. + # Because if face normal data is null, it mean that we need to compute it + # by given vertices. + # The computed vertices is stored in `prebuild_vec_data` and is NOT like `prebuild_face_mtl_idx`, + # we only store valid one in `prebuild_vec_data`. + prebuild_vec_data: list[UTIL_virtools_types.ConstVxVector3 | None] = [] + cache_bv: mathutils.Vector = mathutils.Vector((0, 0, 0)) + for vec_idx in valid_vec_idx: + # but it need mul with transform matrix + cache_bv.x, cache_bv.y, cache_bv.z = _eval_others(proto[TOKEN_VERTICES][vec_idx][TOKEN_VERTICES_DATA], params) + # mul with transform matrix + cache_bv = typing.cast(mathutils.Vector, transform @ cache_bv) + # get result + prebuild_vec_data.append((cache_bv.x, cache_bv.y, cache_bv.z)) + # prepare mesh part data mesh_part: UTIL_blender_mesh.MeshWriterIngredient = UTIL_blender_mesh.MeshWriterIngredient() def vpos_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: - bv: mathutils.Vector = mathutils.Vector((0, 0, 0)) + # simply get data from prebuild vec data v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() - for vec_idx in valid_vec_idx: - # BME no need to convert co system - # but it need mul with transform matrix - bv.x, bv.y, bv.z = _eval_others(proto[TOKEN_VERTICES][vec_idx][TOKEN_VERTICES_DATA], params) - bv = transform @ bv + for vec_data in prebuild_vec_data: + # skip skipped vertices + if vec_data is None: continue # yield result - v.x, v.y, v.z = bv.x, bv.y, bv.z + v.x, v.y, v.z = vec_data yield v mesh_part.mVertexPosition = vpos_iterator() def vnml_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector3]: - # calc normal used transform first + # prepare normal used transform first # ref: https://zhuanlan.zhihu.com/p/96717729 nml_transform: mathutils.Matrix = transform.inverted_safe().transposed() # prepare vars @@ -336,15 +365,34 @@ def create_bme_struct( v: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() for face_idx in valid_face_idx: face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] - for i in range(len(face_data[TOKEN_FACES_INDICES])): - # BME normals need transform by matrix first, - bv.x, bv.y, bv.z = _eval_others(face_data[TOKEN_FACES_NORMALS][i], params) - bv = nml_transform @ bv - # then normalize it - bv.normalize() - # yield result + face_nml_data: list[str] | None = face_data[TOKEN_FACES_NORMALS] + if face_nml_data is None: + # nml is null, we need compute by ourselves + # get first 3 entries in indices list as the compution ref + face_indices_data: list[int] = face_data[TOKEN_FACES_INDICES] + # compute it by getting vertices info from prebuild vertices data + # because the normals is computed from transformed vertices + # so no need to correct its by normal transform. + bv.x, bv.y, bv.z = _compute_normals( + typing.cast(UTIL_virtools_types.ConstVxVector3, prebuild_vec_data[face_indices_data[0]]), + typing.cast(UTIL_virtools_types.ConstVxVector3, prebuild_vec_data[face_indices_data[1]]), + typing.cast(UTIL_virtools_types.ConstVxVector3, prebuild_vec_data[face_indices_data[2]]) + ) + # yield result with N times (N = indices count) v.x, v.y, v.z = bv.x, bv.y, bv.z - yield v + for _ in range(len(face_indices_data)): + yield v + else: + # nml is given, analyse programable fields + for mtl_data in face_nml_data: + # BME normals need transform by matrix first, + bv.x, bv.y, bv.z = _eval_others(mtl_data, params) + bv = typing.cast(mathutils.Vector, nml_transform @ bv) + # then normalize it + bv.normalize() + # yield result + v.x, v.y, v.z = bv.x, bv.y, bv.z + yield v mesh_part.mVertexNormal = vnml_iterator() def vuv_iterator() -> typing.Iterator[UTIL_virtools_types.VxVector2]: v: UTIL_virtools_types.VxVector2 = UTIL_virtools_types.VxVector2() @@ -364,17 +412,17 @@ def create_bme_struct( f: UTIL_blender_mesh.FaceData = UTIL_blender_mesh.FaceData( [UTIL_blender_mesh.FaceVertexData() for i in range(3)] ) - + # create a internal counter to count how many indices has been processed # this counter will be used to calc uv and normal index # because these are based on face, not vertex position index. face_counter: int = 0 - + # iterate valid face for face_idx in valid_face_idx: # get face data face_data: dict[str, typing.Any] = proto[TOKEN_FACES][face_idx] - + # calc indices count face_indices: list[int] = face_data[TOKEN_FACES_INDICES] indices_count: int = len(face_indices) @@ -383,7 +431,7 @@ def create_bme_struct( f.mIndices.pop() while len(f.mIndices) < indices_count: f.mIndices.append(UTIL_blender_mesh.FaceVertexData()) - + # fill the data for i in range(indices_count): # fill vertex position data by indices @@ -391,32 +439,31 @@ def create_bme_struct( # fill nml and uv based on face index f.mIndices[i].mNmlIdx = face_counter + i f.mIndices[i].mUvIdx = face_counter + i - + # add current face indices count to internal counter face_counter += indices_count - + # fill texture data - mtl_name: str = _eval_others(face_data[TOKEN_FACES_TEXTURE], params) - f.mMtlIdx = mtl_remap[mtl_name] - + f.mMtlIdx = prebuild_face_mtl_idx[face_idx] + # return data once yield f mesh_part.mFace = face_iterator() # add part to writer writer.add_ingredient(mesh_part) - + # then we incursively process instance creation for proto_instance in proto[TOKEN_INSTANCES]: # check whether skip this instance if _eval_others(proto_instance[TOKEN_INSTANCES_SKIP], params) == True: continue - + # calc instance params instance_params: dict[str, typing.Any] = {} proto_instance_params: dict[str, str] = proto_instance[TOKEN_INSTANCES_PARAMS] for proto_inst_param_field, proto_inst_param_data in proto_instance_params.items(): instance_params[proto_inst_param_field] = _eval_others(proto_inst_param_data, params) - + # call recursively create_bme_struct( proto_instance[TOKEN_INSTANCES_IDENTIFIER], @@ -428,3 +475,28 @@ def create_bme_struct( ) #endregion + +#region Creation Assist Functions + +def _compute_normals( + point1: UTIL_virtools_types.ConstVxVector3, + point2: UTIL_virtools_types.ConstVxVector3, + point3: UTIL_virtools_types.ConstVxVector3) -> UTIL_virtools_types.ConstVxVector3: + # build vector + p1: mathutils.Vector = mathutils.Vector(point1) + p2: mathutils.Vector = mathutils.Vector(point2) + p3: mathutils.Vector = mathutils.Vector(point3) + + vector1: mathutils.Vector = p2 - p1 + vector2: mathutils.Vector = p3 - p2 + + # do vector x mutiply + # vector1 x vector2 + corss_mul: mathutils.Vector = vector1.cross(vector2) + + # do a normalization + corss_mul.normalize() + return (corss_mul.x, corss_mul.y, corss_mul.z) + + +#endregion diff --git a/bbp_ng/raw_jsons/borders.json b/bbp_ng/raw_jsons/borders.json index 0b36e91..78d75de 100644 Binary files a/bbp_ng/raw_jsons/borders.json and b/bbp_ng/raw_jsons/borders.json differ diff --git a/bbp_ng/raw_jsons/corners.json b/bbp_ng/raw_jsons/corners.json index e7d351c..87de7e6 100644 Binary files a/bbp_ng/raw_jsons/corners.json and b/bbp_ng/raw_jsons/corners.json differ diff --git a/bbp_ng/raw_jsons/sides.json b/bbp_ng/raw_jsons/sides.json index 2434e87..66c60bf 100644 Binary files a/bbp_ng/raw_jsons/sides.json and b/bbp_ng/raw_jsons/sides.json differ diff --git a/bbp_ng/raw_jsons/trafos.json b/bbp_ng/raw_jsons/trafos.json index 67fb8a2..be4425a 100644 Binary files a/bbp_ng/raw_jsons/trafos.json and b/bbp_ng/raw_jsons/trafos.json differ