1 / 1
Dexter Flex Mask — zdjęcie 1

Dexter Flex Mask

Zamów wydruk tego modelu

Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.

Zapisane na tym urządzeniu.

Dexter Flex Mask : Made with flexface.mask.generator.py.

Ocena
0 / 5
Polubienia
5
Pobrania
2
Wydruki
0
Wyświetlenia
29
Oceny
0

Aktualizacja u źródła:

Informacje o modelu

Źródło
Printables
Licencja
Creative Commons — Attribution — Noncommercial — Share Alike

Dexter Flex Mask : Made with flexface.mask.generator.py.

bl_info = {
    "name": "FlexFace Mask Generator",
    "author": "FlexFace Studio",
    "version": (1, 0, 0),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > FlexFace",
    "description": "Unrolls 3D head sculpts into flat-printable flexible masks with preserved 3D feature relief.",
    "category": "Mesh",
}

import bpy import bmesh import math from mathutils import Vector from bpy.props import ( FloatProperty, EnumProperty, BoolProperty, PointerProperty )

=========================================================================

HELPER FUNCTIONS & GEOMETRY GENERATORS

=========================================================================

def get_unit_scale(obj): """Calculates scaling factor (handling meters vs millimeters).""" bbox = [Vector(b) for b in obj.bound_box] h = max(v.z for v in bbox) - min(v.z for v in bbox) return 0.001 if h < 5.0 else 1.0

def create_pill_prism(name, length, width, depth, location, rot_z): """Creates a 2-manifold capsule prism for clean Boolean difference.""" r = width / 2.0 half_l = max(0.0, length - width) / 2.0 segments = 12

pts_2d = []
for i in range(segments + 1):
    ang = -math.pi / 2.0 + math.pi * (i / segments)
    pts_2d.append(Vector((half_l + r * math.cos(ang), r * math.sin(ang))))
for i in range(segments + 1):
    ang = math.pi / 2.0 + math.pi * (i / segments)
    pts_2d.append(Vector((-half_l + r * math.cos(ang), r * math.sin(ang))))

bm = bmesh.new()
half_d = depth / 2.0
b_verts = [bm.verts.new(Vector((p.x, p.y, -half_d))) for p in pts_2d]
t_verts = [bm.verts.new(Vector((p.x, p.y, half_d))) for p in pts_2d]

bm.faces.new(reversed(b_verts))
bm.faces.new(t_verts)
n = len(pts_2d)
for i in range(n):
    next_i = (i + 1) % n
    bm.faces.new([b_verts[i], b_verts[next_i], t_verts[next_i], t_verts[i]])

bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
mesh = bpy.data.meshes.new(f"{name}_Mesh")
bm.to_mesh(mesh)
bm.free()

obj = bpy.data.objects.new(name, mesh)
obj.location = location
obj.rotation_euler.z = rot_z
bpy.context.collection.objects.link(obj)
return obj

def build_guide_mesh(source_obj, props): """Generates wireframe visualization showing the capture bounds and strap slots.""" bbox = [Vector(b) for b in source_obj.bound_box] min_x, max_x = min(v.x for v in bbox), max(v.x for v in bbox) min_y, max_y = min(v.y for v in bbox), max(v.y for v in bbox) min_z, max_z = min(v.z for v in bbox), max(v.z for v in bbox)

head_w = max_x - min_x
head_d = max_y - min_y
head_h = max_z - min_z

cx = (min_x + max_x) / 2.0
cz = min_z + (head_h * props.center_z_ratio)
rad_x = (head_w * props.mask_width_ratio) / 2.0
rad_z = (head_h * props.mask_height_ratio) / 2.0

bm = bmesh.new()
segments = 48
ring_verts = []

# Ovoid front projection curve
for i in range(segments):
    phi = (2.0 * math.pi * i) / segments
    rx = rad_x * (1.0 + props.egg_taper * math.sin(phi))
    px = cx + rx * math.cos(phi)
    pz = cz + rad_z * math.sin(phi)
    # Position slightly forward of bust
    py = min_y - (head_d * 0.05) if props.forward_axis == "-Y" else max_y + (head_d * 0.05)
    ring_verts.append(bm.verts.new(Vector((px, py, pz))))

for i in range(segments):
    bm.edges.new((ring_verts[i], ring_verts[(i + 1) % segments]))

mesh = bpy.data.meshes.new("FlexFace_Guide_Mesh")
bm.to_mesh(mesh)
bm.free()
return mesh

=========================================================================

CORE EXTRACTION PIPELINE

=========================================================================

def execute_mask_generation(context): props = context.scene.flexface_props source_obj = context.active_object

if not source_obj or source_obj.type != 'MESH':
    raise RuntimeError("Select your head or bust mesh.")

# Remove previous run
if "Face_Unrolled_Mask" in bpy.data.objects:
    old = bpy.data.objects["Face_Unrolled_Mask"]
    old_mesh = old.data
    bpy.data.objects.remove(old, do_unlink=True)
    bpy.data.meshes.remove(old_mesh, do_unlink=True)

bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)

# Duplicate
bpy.ops.object.duplicate()
mask_obj = context.active_object
mask_obj.name = "Face_Unrolled_Mask"

bbox = [Vector(b) for b in mask_obj.bound_box]
min_x, max_x = min(v.x for v in bbox), max(v.x for v in bbox)
min_y, max_y = min(v.y for v in bbox), max(v.y for v in bbox)
min_z, max_z = min(v.z for v in bbox), max(v.z for v in bbox)

head_w = max_x - min_x
head_d = max_y - min_y
head_h = max_z - min_z

scale = get_unit_scale(mask_obj)
mask_thickness = props.thickness_mm * scale
slot_w = props.slot_width_mm * scale
slot_h = props.slot_height_mm * scale
wall_margin = props.wall_margin_mm * scale

cx = (min_x + max_x) / 2.0
cz = min_z + (head_h * props.center_z_ratio)
rad_x = (head_w * props.mask_width_ratio) / 2.0
rad_y = (head_h * props.mask_height_ratio) / 2.0

# 1. Isolation
y_cutoff = min_y + (head_d * 0.60) if props.forward_axis == "-Y" else max_y - (head_d * 0.60)
bm = bmesh.new()
bm.from_mesh(mask_obj.data)
bm.faces.ensure_lookup_table()

faces_to_delete = []
for f in bm.faces:
    c = f.calc_center_median()
    is_front = c.y &lt; y_cutoff if props.forward_axis == "-Y" else c.y &gt; y_cutoff
    if not is_front or abs(c.x - cx) &gt; (rad_x * 1.45) or abs(c.z - cz) &gt; (rad_y * 1.35):
        faces_to_delete.append(f)

bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')

verts_to_delete = [v for v in bm.verts if len(v.link_faces) == 0]
bmesh.ops.delete(bm, geom=verts_to_delete, context='VERTS')

# 2. Continuous Linear Regression Baseline (Zero Banding)
axis_y = min_y + (head_d * 0.52)
bm.verts.ensure_lookup_table()

cheek_samples = []
vert_info = []

for v in bm.verts:
    dx = v.co.x - cx
    dy = max(1e-6, axis_y - v.co.y) if props.forward_axis == "-Y" else max(1e-6, v.co.y - axis_y)
    r = math.sqrt(dx * dx + dy * dy)
    theta = math.atan2(dx, dy)
    rel_y = v.co.z - cz

    vert_info.append((dx, dy, r, theta, rel_y))
    if abs(dx) &gt; (rad_x * 0.32):
        cheek_samples.append((rel_y, r))

if not cheek_samples:
    cheek_samples = [(p[4], p[2]) for p in vert_info]

mean_y = sum(p[0] for p in cheek_samples) / len(cheek_samples)
mean_r = sum(p[1] for p in cheek_samples) / len(cheek_samples)
denom = sum((p[0] - mean_y) ** 2 for p in cheek_samples)
slope = sum((p[0] - mean_y) * (p[1] - mean_r) for p in cheek_samples) / max(denom, 1e-6)
intercept = mean_r - slope * mean_y

residuals = sorted([p[1] - (slope * p[0] + intercept) for p in cheek_samples])
r_offset = residuals[int(len(residuals) * 0.15)]

# 3. Isometric Arc-Length Flattening
for idx, v in enumerate(bm.verts):
    dx, dy, r, theta, rel_y = vert_info[idx]
    r_base_smooth = (slope * rel_y + intercept) + r_offset

    v.co.x = r * theta                     # 1:1 true surface width
    v.co.y = rel_y
    v.co.z = max(0.0, r - r_base_smooth)   # Smooth 3D depth

# 4. Mathematical Ovoid Boundary Cut
bm.faces.ensure_lookup_table()
ovoid_prune = []
for f in bm.faces:
    c = f.calc_center_median()
    y_clamped = max(-rad_y, min(rad_y, c.y))
    rx_local = rad_x * (1.0 + props.egg_taper * (y_clamped / rad_y))
    if (c.x / rx_local) ** 2 + (c.y / rad_y) ** 2 &gt; 1.0:
        ovoid_prune.append(f)

bmesh.ops.delete(bm, geom=ovoid_prune, context='FACES')

verts_to_delete = [v for v in bm.verts if len(v.link_faces) == 0]
bmesh.ops.delete(bm, geom=verts_to_delete, context='VERTS')

# 5. Adhesion Perimeter Falloff (Z = 0)
for v in bm.verts:
    y_clamped = max(-rad_y, min(rad_y, v.co.y))
    rx_local = rad_x * (1.0 + props.egg_taper * (y_clamped / rad_y))
    d_norm = math.sqrt((v.co.x / rx_local) ** 2 + (v.co.y / rad_y) ** 2)

    if d_norm &lt;= 0.72:
        w = 1.0
    elif d_norm &gt;= 0.96:
        w = 0.0
    else:
        t = (d_norm - 0.72) / (0.96 - 0.72)
        w = 1.0 - (t * t * (3.0 - 2.0 * t))
    v.co.z *= w

bm.faces.ensure_lookup_table()
if sum(f.normal.z for f in bm.faces) / len(bm.faces) &lt; 0:
    bmesh.ops.reverse_faces(bm, faces=bm.faces)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

bm.to_mesh(mask_obj.data)
bm.free()
mask_obj.data.update()
context.view_layer.update()

# 6. Solidify
bpy.ops.object.select_all(action='DESELECT')
mask_obj.select_set(True)
context.view_layer.objects.active = mask_obj

solid_mod = mask_obj.modifiers.new(name="Shell_Thickness", type='SOLIDIFY')
solid_mod.thickness = mask_thickness
solid_mod.offset = 1.0
bpy.ops.object.modifier_apply(modifier=solid_mod.name)

# 7. Tangential Slots
slot_angles = [
    props.upper_slot_angle,
    -props.upper_slot_angle,
    props.lower_slot_angle,
    -props.lower_slot_angle
]

cutters = []
cutter_depth = head_h * 0.5

for idx, ang_deg in enumerate(slot_angles):
    phi = math.radians(ang_deg)
    rx_local = rad_x * (1.0 + props.egg_taper * math.cos(phi))
    px = rx_local * math.sin(phi)
    py = rad_y * math.cos(phi)

    dx_dphi = rad_x * (math.cos(phi) + props.egg_taper * math.cos(2.0 * phi))
    dy_dphi = -rad_y * math.sin(phi)
    t_len = math.sqrt(dx_dphi * dx_dphi + dy_dphi * dy_dphi)

    tx, ty = dx_dphi / t_len, dy_dphi / t_len
    nx, ny = ty, -tx
    if (px * nx + py * ny) &gt; 0:
        nx, ny = -nx, -ny

    inward_offset = wall_margin + (slot_w / 2.0)
    sx = px + nx * inward_offset
    sy = py + ny * inward_offset
    tangent_angle = math.atan2(ty, tx)

    c = create_pill_prism(
        f"Pill_Slot_{idx}", slot_h, slot_w, cutter_depth,
        Vector((sx, sy, 0.0)), tangent_angle
    )
    cutters.append(c)

bpy.ops.object.select_all(action='DESELECT')
for c in cutters:
    c.select_set(True)
context.view_layer.objects.active = cutters[0]
bpy.ops.object.join()
cutter_tool = context.active_object

bpy.ops.object.select_all(action='DESELECT')
mask_obj.select_set(True)
context.view_layer.objects.active = mask_obj

bool_mod = mask_obj.modifiers.new(name="Strap_Slots", type='BOOLEAN')
bool_mod.object = cutter_tool
bool_mod.operation = 'DIFFERENCE'
try:
    bool_mod.solver = 'MANIFOLD'
except TypeError:
    bool_mod.solver = 'EXACT'

bpy.ops.object.modifier_apply(modifier=bool_mod.name)
bpy.data.objects.remove(cutter_tool, do_unlink=True)

mask_obj.location.x = -(head_w * 1.4)

=========================================================================

OPERATORS & UI PROPERTIES

=========================================================================

def update_guide_event(self, context): """Triggers guide redraw when any slider changes in the UI.""" if context.scene.flexface_props.show_guide: bpy.ops.flexface.toggle_guide('EXEC_DEFAULT', enable=True)

class FlexFaceProperties(bpy.types.PropertyGroup): show_guide: BoolProperty( name="Show Alignment Guide", description="Displays wireframe framing oval on the bust", default=False ) forward_axis: EnumProperty( name="Forward Axis", description="Axis pointing straight out from the nose", items=[ ("-Y", "-Y (Standard Front)", "Front view faces -Y"), ("+Y", "+Y", "Faces +Y"), ], default="-Y", update=update_guide_event ) mask_width_ratio: FloatProperty( name="Width Ratio", description="Mask width relative to head width", default=0.68, min=0.45, max=0.95, update=update_guide_event ) mask_height_ratio: FloatProperty( name="Height Ratio", description="Mask height relative to head height", default=0.80, min=0.50, max=0.95, update=update_guide_event ) center_z_ratio: FloatProperty( name="Center Z Ratio", description="Vertical focal position (philtrum / midface)", default=0.43, min=0.25, max=0.65, update=update_guide_event ) egg_taper: FloatProperty( name="Egg Taper", description="Widens brow and tapers jawline", default=0.10, min=0.0, max=0.25, update=update_guide_event ) thickness_mm: FloatProperty( name="Shell Thickness (mm)", description="Extruded thickness for TPU print", default=1.4, min=0.8, max=3.0 ) wall_margin_mm: FloatProperty( name="Wall Margin (mm)", description="Solid border distance between strap slot and edge", default=8.5, min=4.0, max=16.0 ) slot_width_mm: FloatProperty( name="Slot Width (mm)", description="Strap band thickness clearance", default=3.2, min=1.5, max=6.0 ) slot_height_mm: FloatProperty( name="Slot Height (mm)", description="Strap band width (length of slot)", default=12.0, min=6.0, max=25.0 ) upper_slot_angle: FloatProperty( name="Upper Slot Angle", description="Angle from top for temple straps", default=46.0, min=25.0, max=65.0 ) lower_slot_angle: FloatProperty( name="Lower Slot Angle", description="Angle from top for jawline straps", default=136.0, min=105.0, max=165.0 )

class FLEXFACE_OT_ToggleGuide(bpy.types.Operator): """Toggles or updates the visual alignment guide in the 3D viewport""" bl_idname = "flexface.toggle_guide" bl_label = "Update Alignment Guide" bl_options = {'REGISTER', 'UNDO'}

enable: BoolProperty(default=True)

def execute(self, context):
    props = context.scene.flexface_props
    # Delete existing guide
    if "FlexFace_Guide" in bpy.data.objects:
        g = bpy.data.objects["FlexFace_Guide"]
        gm = g.data
        bpy.data.objects.remove(g, do_unlink=True)
        bpy.data.meshes.remove(gm, do_unlink=True)

    if not self.enable:
        props.show_guide = False
        return {'FINISHED'}

    active = context.active_object
    if not active or active.type != 'MESH':
        self.report({'WARNING'}, "Select a head or bust mesh first.")
        return {'CANCELLED'}

    props.show_guide = True
    guide_mesh = build_guide_mesh(active, props)
    guide_obj = bpy.data.objects.new("FlexFace_Guide", guide_mesh)
    context.collection.objects.link(guide_obj)
    guide_obj.display_type = 'WIRE'
    guide_obj.show_in_front = True
    guide_obj.hide_render = True
    return {'FINISHED'}

class FLEXFACE_OT_GenerateMask(bpy.types.Operator): """Processes the head mesh and generates the flat flexible mask""" bl_idname = "flexface.generate_mask" bl_label = "Generate Flat Mask" bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
    try:
        execute_mask_generation(context)
        self.report({'INFO'}, "Flat Flexible Mask generated successfully.")
        return {'FINISHED'}
    except Exception as e:
        self.report({'ERROR'}, str(e))
        return {'CANCELLED'}

class FLEXFACE_PT_MainPanel(bpy.types.Panel): """Sidebar N-Panel for FlexFace controls""" bl_label = "FlexFace Mask Generator" bl_idname = "FLEXFACE_PT_main_panel" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'FlexFace'

def draw(self, context):
    layout = self.layout
    props = context.scene.flexface_props

    # Alignment Guide Section
    box = layout.box()
    box.label(text="1. Alignment &amp; Framing", icon='OUTLINER_OB_CAMERA')
    if not props.show_guide:
        op = box.operator("flexface.toggle_guide", text="Show Alignment Guide", icon='HIDE_OFF')
        op.enable = True
    else:
        op = box.operator("flexface.toggle_guide", text="Hide Alignment Guide", icon='HIDE_ON')
        op.enable = False

    col = box.column(align=True)
    col.prop(props, "forward_axis")
    col.prop(props, "center_z_ratio", slider=True)
    col.prop(props, "mask_width_ratio", slider=True)
    col.prop(props, "mask_height_ratio", slider=True)
    col.prop(props, "egg_taper", slider=True)

    # Print &amp; Hardware Specs
    box2 = layout.box()
    box2.label(text="2. Shell &amp; Strap Slots", icon='MOD_SOLIDIFY')
    col2 = box2.column(align=True)
    col2.prop(props, "thickness_mm")
    col2.prop(props, "wall_margin_mm")
    col2.prop(props, "slot_height_mm")
    col2.prop(props, "slot_width_mm")

    row = box2.row(align=True)
    row.prop(props, "upper_slot_angle")
    row.prop(props, "lower_slot_angle")

    # Action Button
    layout.separator()
    big_btn = layout.operator("flexface.generate_mask", text="Generate Flat Mask", icon='MOD_SMOOTH')
    big_btn.scale_y = 1.6

=========================================================================

REGISTRATION

=========================================================================

classes = ( FlexFaceProperties, FLEXFACE_OT_ToggleGuide, FLEXFACE_OT_GenerateMask, FLEXFACE_PT_MainPanel, )

def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.flexface_props = PointerProperty(type=FlexFaceProperties)

def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.flexface_props

if name == "main": register()