from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils

import math
from typing import Optional
from dataclasses import dataclass, field
from enum import Enum
import cadquery as Cq

class AttachPoint(Enum):
    DOVETAIL_IN = 1
    DOVETAIL_OUT = 2
    NONE = 3
    # Inset slot for front surface attachment       j
    SLOT = 4

@dataclass
class Crown(Model):

    facets: int = 5
    # Lower circumference
    base_circ: float = 538.0
    # Upper circumference, at the middle
    tilt_circ: float = 640.0
    front_base_circ: float = (640.0 + 538.0) / 2
    # Total height
    height: float = 120.0

    # Front guard has a wing that inserts into the side guards.
    front_wing_angle: float = 9.0
    front_wing_dh: float = 40.0
    front_wing_height: float = 20.0

    margin: float = 10.0

    thickness: float = 0.4 # 26 Gauge
    side_guard_thickness: float = 15.0
    side_guard_channel_radius: float = 90
    side_guard_channel_height: float = 10
    side_guard_hole_height: float = 15.0
    side_guard_hole_diam: float = 1.5
    side_guard_dovetail_height: float = 30.0

    side_guard_slot_width: float = 22.0
    side_guard_slot_angle: float = 18.0
    # brass insert thickness
    slot_thickness: float = 2.0
    slot_width: float = 20.0
    slot_tilt: float = 60

    material: Material = Material.METAL_BRASS
    material_side: Material = Material.PLASTIC_PLA

    def __post_init__(self):
        super().__init__(name="crown")

        assert self.tilt_circ > self.base_circ
        assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees"
        assert self.side_guard_channel_radius > self.radius_lower

        assert self.front_wing_angle < 180 / self.facets
        assert self.front_wing_dh + self.front_wing_height < self.height
        assert self.slot_phi < 2 * math.pi / self.facets

    @property
    def facet_width_lower(self):
        return self.base_circ / self.facets
    @property
    def facet_width_upper(self):
        return self.tilt_circ / self.facets
    @property
    def radius_lower(self):
        return self.base_circ / (2 * math.pi)
    @property
    def radius_middle(self):
        return self.tilt_circ / (2 * math.pi)
    @property
    def radius_upper(self):
        return (self.tilt_circ + (self.tilt_circ - self.base_circ)) / (2 * math.pi)

    @property
    def radius_lower_front(self):
        return self.front_base_circ / (2 * math.pi)
    @property
    def radius_middle_front(self):
        return self.radius_lower_front + (self.radius_middle - self.radius_lower)
    @property
    def radius_upper_front(self):
        return self.radius_lower_front + (self.radius_upper - self.radius_lower)

    @property
    def slot_r0(self):
        return self.radius_lower + self.thickness / 2
    @property
    def slot_r1(self):
        return self.radius_upper + self.thickness / 2

    @property
    def slot_h0(self) -> float:
        """
        Phantom height formed by similar triangle, i.e. h0 in

        (h0 + h) / r2 = h0 / r1
        """
        rat = self.slot_r0  / (self.slot_r1 - self.slot_r0)
        return self.height * rat
    @property
    def slot_outer_h0(self):
        rat = (self.slot_r0 + self.side_guard_thickness) / (self.slot_r1 - self.slot_r0)
        return self.height * rat
    @property
    def slot_theta(self) -> float:
        """
        Cone tilt, related to other quantities by
        h0 = r1 * cot theta
        """
        h = self.height
        return math.atan(self.slot_r0 / (self.height + self.slot_h0))
    @property
    def slot_phi(self) -> float:
        """
        When a slice of the crown is expanded (via Gauss's Theorema Egregium),
        it does not form a full circle. phi is the angle of one of the slices.

        Note that on the cone itself, the angular slice is `2 pi / n` which `n`
        is the number of sides.
        """
        arc = self.slot_r0 * math.pi * 2 / self.facets
        rho = self.slot_h0 / math.cos(self.slot_theta)
        return arc / rho


    def profile_base(self) -> Cq.Sketch:
        # Generate a conical pentagonal shape

        y0 = self.slot_h0 / math.cos(self.slot_theta)
        yh = (self.height/2 + self.slot_h0) / math.cos(self.slot_theta)
        yq = (self.height*3/4 + self.slot_h0) / math.cos(self.slot_theta)
        y1 = (self.height + self.slot_h0) / math.cos(self.slot_theta)
        phi2 = self.slot_phi / 2

        return (
            Cq.Sketch()
            .segment(
                (y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
                (yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
            )
            .arc(
                (yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
                (yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
                (0, y1 - y0),
            )
            .arc(
                (-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
                (-yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
                (0, y1 - y0),
            )
            .segment(
                (-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
                (-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
            )
            .arc(
                (y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2)),
                (0, 0),
                (-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
            )
            .assemble()
        )

    @target(name="eye", kind=TargetKind.DXF)
    def profile_eye(self) -> Cq.Sketch:
        """
        deprecated
        """
        dy = self.facet_width_upper * 0.1
        y_tip = self.height - self.margin

        eye = (
            Cq.Sketch()
            .segment(
                (0, y_tip),
                (dy, y_tip - dy),
            )
            .segment(
                (0, y_tip),
                (-dy, y_tip - dy),
            )
            .bezier([
                (dy, y_tip - dy),
                (dy/2, y_tip - dy*.6),
                (dy/4, y_tip - dy/2),
                (0, y_tip - dy/2),
            ])
            .bezier([
                (0, y_tip - dy/2),
                (-dy/4, y_tip - dy/2),
                (-dy/2, y_tip - dy*.6),
                (-dy, y_tip - dy),
            ])
            .assemble()
        )
        return eye

    @target(name="dot", kind=TargetKind.DXF)
    def profile_dot(self) -> Cq.Sketch:
        return (
            Cq.Sketch()
            .circle(self.margin / 2)
        )

    def profile_front_wing(self, mirror: bool) -> Cq.Sketch:
        """
        These two wings help the front profile attach
        """
        hw = self.front_wing_height / math.cos(self.slot_theta)
        hw0 = (self.front_wing_dh + self.slot_h0) / math.cos(self.slot_theta)
        hw1 = hw0 + hw
        y0 = self.slot_h0 / math.cos(self.slot_theta)
        # Calculate angle of wing analogously to `this.slot_phi`. This arc's
        # radius is hw0.
        wing_arc = self.slot_r0 * math.radians(self.front_wing_angle)
        phi_w = wing_arc / hw0
        sign = -1 if mirror else 1
        phi2 = self.slot_phi / 2
        return (
            Cq.Sketch()
            .segment(
                (sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
                (sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
            )
            .segment(
                (sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
                (sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
            )
            .arc(
                (sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
                (sign * hw0 * math.sin(phi2+phi_w/2), -y0 + hw0 * math.cos(phi2+phi_w/2)),
                (sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
            )
            .arc(
                (sign * hw1 * math.sin(phi2),         -y0 + hw1 * math.cos(phi2)),
                (sign * hw1 * math.sin(phi2+phi_w/2), -y0 + hw1 * math.cos(phi2+phi_w/2)),
                (sign * hw1 * math.sin(phi2+phi_w),   -y0 + hw1 * math.cos(phi2+phi_w)),
            )
            .assemble()
        )


    @target(name="front", kind=TargetKind.DXF)
    def profile_front(self) -> Cq.Sketch:
        """
        Front profile slots into holes on the side guards
        """
        profile_base = (
            self.profile_base()
            .boolean(self.profile_front_wing(False), mode='a')
            .boolean(self.profile_front_wing(True), mode='a')
        )


        dx_l = self.facet_width_lower
        dx_u = self.facet_width_upper
        dy = self.height

        window_length = dy / 5
        window_height = self.margin / 2
        window = (
            Cq.Sketch()
            .rect(window_length, window_height)
        )
        window_p1 = Cq.Location.from2d(
            dx_u/2 - self.margin - window_length * 0.4,
            dy/2 + self.margin/2,
            math.degrees(math.atan2(dy/2, -dx_u/2) * 0.95),
        )
        window_p2 = Cq.Location.from2d(
            dx_l/2 - self.margin + window_length * 0.15,
            window_length/2 + self.margin,
            math.degrees(math.atan2(dy/2, (dx_u-dx_l)/2)),
        )

        # Carve the scale
        z = dy * 1/32 # "Pen" Thickness
        scale_pan_x = dx_l / 2 * 0.6
        scale_pan_y = dy / 2 * 0.7
        pan_dx = dx_l * 1/4
        pan_dy = dy * 1/16

        scale_pan = (
            Cq.Sketch()
            .arc(
                (- pan_dx/2, pan_dy),
                (0, 0),
                (+ pan_dx/2, pan_dy),
            )
            .segment(
                (+pan_dx/2, pan_dy),
                (+pan_dx/2 - z, pan_dy),
            )
            .arc(
                (-pan_dx/2 + z, pan_dy),
                (0, z),
                (+pan_dx/2 - z, pan_dy),
            )
            .segment(
                (-pan_dx/2, pan_dy),
                (-pan_dx/2 + z, pan_dy),
            )
            .assemble()
        )
        loc_scale_pan = Cq.Location.from2d(scale_pan_x, scale_pan_y)
        loc_scale_pan2 = Cq.Location.from2d(-scale_pan_x, scale_pan_y)

        scale_base_y = dy / 2 * 0.36
        scale_base_x = dx_l / 10
        assert scale_base_y < scale_pan_y
        assert scale_base_x < scale_pan_x

        scale_body = (
            Cq.Sketch()
            .arc(
                (scale_pan_x, scale_pan_y),
                (0, scale_base_y),
                (-scale_pan_x, scale_pan_y),
            )
            .segment(
                (-scale_pan_x, scale_pan_y),
                (-scale_pan_x+z, scale_pan_y+z),
            )
            .arc(
                (scale_pan_x - z, scale_pan_y+z),
                (0, scale_base_y + z),
                (-scale_pan_x + z, scale_pan_y+z),
            )
            .segment(
                (scale_pan_x, scale_pan_y),
                (scale_pan_x-z, scale_pan_y+z),
            )
            .assemble()
            .polygon([
                (scale_base_x, scale_base_y + z/2),
                (scale_base_x, self.margin),
                (scale_base_x-z, self.margin),
                (scale_base_x-z, scale_base_y-z),

                (-scale_base_x+z, scale_base_y-z),
                (-scale_base_x+z, self.margin),
                (-scale_base_x, self.margin),
                (-scale_base_x, scale_base_y + z/2),
            ], mode='a')
        )

        # Needle
        needle_y_top = dy - self.margin
        needle_y_mid = dy * 0.7
        needle_dx = scale_base_x * 2
        y_shoulder = needle_y_mid - z * 2
        needle = (
            Cq.Sketch()
            .segment(
                (0, needle_y_mid),
                (z, y_shoulder),
            )
            .segment(
                (z, y_shoulder),
                (z, scale_base_y),
            )
            .segment(
                (z, scale_base_y),
                (-z, scale_base_y),
            )
            .segment(
                (-z, y_shoulder),
                (-z, scale_base_y),
            )
            .segment(
                (-z, y_shoulder),
                (0, needle_y_mid),
            )
            .assemble()
        )
        z2 = z * 2
        y1 = needle_y_mid + z2
        needle_head = (
            Cq.Sketch()
            .segment(
                (z, needle_y_mid),
                (z, y1),
            )
            .segment(
                (-z, needle_y_mid),
                (-z, y1),
            )
            # Outer edge
            .bezier([
                (0, needle_y_top),
                (0, (needle_y_top + needle_y_mid)/2),
                (needle_dx, (needle_y_top + needle_y_mid)/2),
                (z, needle_y_mid),
            ])
            .bezier([
                (0, needle_y_top),
                (0, (needle_y_top + needle_y_mid)/2),
                (-needle_dx, (needle_y_top + needle_y_mid)/2),
                (-z, needle_y_mid),
            ])
            # Inner edge
            .bezier([
                (0, needle_y_top - z2),
                (0, (needle_y_top + needle_y_mid)/2),
                (needle_dx-z2*2, (needle_y_top + needle_y_mid)/2),
                (z, y1),
            ])
            .bezier([
                (0, needle_y_top - z2),
                (0, (needle_y_top + needle_y_mid)/2),
                (-needle_dx+z2*2, (needle_y_top + needle_y_mid)/2),
                (-z, y1),
            ])
            .assemble()
        )

        return (
            profile_base
            .boolean(window.moved(window_p1), mode='s')
            .boolean(window.moved(window_p1.flip_x()), mode='s')
            .boolean(window.moved(window_p2), mode='s')
            .boolean(window.moved(window_p2.flip_x()), mode='s')
            .boolean(scale_pan.moved(loc_scale_pan), mode='s')
            .boolean(scale_pan.moved(loc_scale_pan2), mode='s')
            .boolean(scale_body, mode='s')
            .boolean(needle, mode='s')
            .boolean(needle_head, mode='s')
            .clean()
        )

    @target(name="side-guard", kind=TargetKind.DXF)
    def profile_side_guard(self) -> Cq.Sketch:
        dx = self.facet_width_lower / 2
        dy = self.height

        # Main control points
        p_mid = Cq.Location.from2d(0, 0.5 * dy)
        p_mid_v = Cq.Location.from2d(10/57 * dx, 0)
        p_top1 = Cq.Location.from2d(0.408 * dx, 5/24 * dy)
        p_top1_v = Cq.Location.from2d(0.13 * dx, 0)
        p_top2 = Cq.Location.from2d(0.737 * dx, 0.255 * dy)
        p_top2_c1 = p_top2 * Cq.Location.from2d(-0.105 * dx, 0.033 * dy)
        p_top2_c2 = p_top2 * Cq.Location.from2d(-0.053 * dx, -0.09 * dy)
        p_top3 = Cq.Location.from2d(0.929 * dx, 0.145 * dy)
        p_top3_v = Cq.Location.from2d(0.066 * dx, 0.033 * dy)
        p_top4 = Cq.Location.from2d(0.85 * dx, 0.374 * dy)
        p_top4_v = Cq.Location.from2d(-0.053 * dx, 0.008 * dy)
        p_top5 = Cq.Location.from2d(0.54 * dx, 0.349 * dy)
        p_top5_c1 = p_top5 * Cq.Location.from2d(0.103 * dx, 0.017 * dy)
        p_top5_c2 = p_top5 * Cq.Location.from2d(0.158 * dx, 0.034 * dy)
        p_base_c = Cq.Location.from2d(1.5 * dx, 0.55 * dy)

        y0 = self.slot_outer_h0 / math.cos(self.slot_theta)
        phi2 = self.slot_phi / 2
        p_base = Cq.Location.from2d(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2))

        bezier_groups = [
            [
                p_base,
                p_base_c,
                p_top5_c2,
                p_top5,
            ],
            [
                p_top5,
                p_top5_c1,
                p_top4 * p_top4_v,
                p_top4,
            ],
            [
                p_top4,
                p_top4 * p_top4_v.inverse.scale(4),
                p_top3 * p_top3_v,
                p_top3,
            ],
            [
                p_top3,
                p_top3 * p_top3_v.inverse,
                p_top2_c2,
                p_top2,
            ],
            [
                p_top2,
                p_top2_c1,
                p_top1 * p_top1_v,
                p_top1,
            ],
            [
                p_top1,
                p_top1 * p_top1_v.inverse,
                p_mid * p_mid_v,
                p_mid,
            ],
        ]
        sketch = (
            Cq.Sketch()
            .arc(
                p_base.to2d_pos(),
                (0, 0),
                p_base.flip_x().to2d_pos(),
            )
        )
        for bezier_group in bezier_groups:
            sketch = (
                sketch
                .bezier([p.to2d_pos() for p in bezier_group])
                .bezier([p.flip_x().to2d_pos() for p in bezier_group])
            )
        return sketch.assemble()

    def side_guard_dovetail(self) -> Cq.Solid:
        """
        Generates a dovetail coupling for the side guard
        """
        dx = self.side_guard_thickness / 2
        wire = Cq.Wire.makePolygon([
            (dx * 0.5, 0),
            (dx * 0.7, dx),
            (-dx * 0.7, dx),
            (-dx * 0.5, 0),
        ], close=True)
        return Cq.Solid.extrudeLinear(
            wire,
            [],
            (0,0,dx + self.side_guard_dovetail_height),
        ).moved((0, 0, -dx))

    def side_guard_frontal_slot(self) -> Cq.Workplane:
        angle = 360 / self.facets
        inner_d = self.thickness / 2 - self.slot_thickness / 2
        outer_d = self.thickness / 2 + self.slot_thickness / 2
        outer = Cq.Solid.makeCone(
            radius1=self.radius_lower_front + outer_d,
            radius2=self.radius_upper_front + outer_d,
            height=self.height,
            angleDegrees=angle,
        )
        inner = Cq.Solid.makeCone(
            radius1=self.radius_lower_front + inner_d,
            radius2=self.radius_upper_front + inner_d,
            height=self.height,
            angleDegrees=angle,
        )
        shell = (
            outer.cut(inner)
            .rotate((0,0,0), (0,0,1), -angle/2)
        )
        # Generate the sector intersector
        intersector = Cq.Solid.makeCylinder(
            radius=self.radius_upper + self.side_guard_thickness,
            height=self.front_wing_height,
            angleDegrees=self.front_wing_angle,
        ).moved(Cq.Location(0,0,self.front_wing_dh,0,0,-self.front_wing_angle/2))
        return shell * intersector

    def side_guard(
            self,
            attach_left: AttachPoint,
            attach_right: AttachPoint,
        ) -> Cq.Workplane:
        """
        Constructs the side guard using a cone. Via Gauss's Theorema Egregium,
        the surface of the cone can be deformed into a plane.
        """
        angle_span = 360 / self.facets
        outer = Cq.Solid.makeCone(
            radius1=self.radius_lower + self.side_guard_thickness,
            radius2=self.radius_upper + self.side_guard_thickness,
            height=self.height,
            angleDegrees=angle_span,
        )
        inner = Cq.Solid.makeCone(
            radius1=self.radius_lower,
            radius2=self.radius_upper,
            height=self.height,
            angleDegrees=angle_span,
        )
        shell = (outer - inner).rotate((0,0,0), (0,0,1), -angle_span/2)
        dx = math.sin(math.radians(angle_span / 2)) * (self.radius_middle + self.side_guard_thickness)
        profile = (
            Cq.Workplane('YZ')
            .polyline([
                (0, self.height),
                (-dx, self.height / 2),
                (-dx, 0),
                (dx, 0),
                (dx, self.height / 2),
            ])
            .close()
            .extrude(self.radius_upper + self.side_guard_thickness)
            .val()
        )
        #channel = (
        #    Cq.Solid.makeCylinder(
        #        radius=self.side_guard_channel_radius + 1.0,
        #        height=self.side_guard_channel_height,
        #    ) - Cq.Solid.makeCylinder(
        #        radius=self.side_guard_channel_radius,
        #        height=self.side_guard_channel_height,
        #    )
        #)
        result = shell * profile# - channel

        # Create the downward slots
        for sign in [-1, 1]:
            slot_box = Cq.Solid.makeBox(
                length=self.height,
                width=self.slot_width,
                height=self.slot_thickness,
            ).moved(
                Cq.Location(-self.slot_thickness,-self.slot_width/2, -self.slot_thickness/2)
            )
            # keyhole for threads to stay in place
            slot_cyl = Cq.Solid.makeCylinder(
                radius=self.slot_thickness/2,
                height=self.height,
                pnt=(0,0,self.slot_thickness/2),
                dir=(1,0,0),
            )
            slot = slot_box + slot_cyl
            slot = slot.moved(
                Cq.Location.rot2d(sign * self.side_guard_slot_angle) *
                Cq.Location(self.radius_lower + self.side_guard_thickness/2, 0, 0) *
                Cq.Location(0,0,0,0,-180 + self.slot_tilt,0)
            )
            result = result - slot

        radius_attach = self.radius_lower + self.side_guard_thickness / 2
        # tilt the dovetail by radius differential
        angle_tilt = math.degrees(math.atan2(self.radius_middle - self.radius_lower, self.height / 2))
        dovetail = self.side_guard_dovetail()
        loc_dovetail_left = Cq.Location.rot2d(angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
        loc_dovetail_right = Cq.Location.rot2d(-angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)

        angle_slot = 180 / self.facets - self.front_wing_angle / 2
        match attach_left:
            case AttachPoint.DOVETAIL_IN:
                loc_dovetail_left *= Cq.Location.rot2d(180)
                result = result - dovetail.moved(loc_dovetail_left)
            case AttachPoint.DOVETAIL_OUT:
                result = result + dovetail.moved(loc_dovetail_left)
            case AttachPoint.SLOT:
                result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(angle_slot))
            case AttachPoint.NONE:
                pass
        match attach_right:
            case AttachPoint.DOVETAIL_IN:
                result = result - dovetail.moved(loc_dovetail_right)
            case AttachPoint.DOVETAIL_OUT:
                loc_dovetail_right *= Cq.Location.rot2d(180)
                result = result + dovetail.moved(loc_dovetail_right)
            case AttachPoint.SLOT:
                result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(-angle_slot))
            case AttachPoint.NONE:
                pass
        # Remove parts below the horizontal
        cut_h = self.radius_lower
        result -= Cq.Solid.makeCylinder(
            radius=self.radius_lower + self.side_guard_thickness,
            height=cut_h).moved((0,0,-cut_h))
        return result

    @target(name="side_guard_1", angularTolerance=0.01)
    def side_guard_1(self) -> Cq.Workplane:
        return self.side_guard(
            attach_left=AttachPoint.SLOT,
            attach_right=AttachPoint.DOVETAIL_IN,
        )
    @target(name="side_guard_2", angularTolerance=0.01)
    def side_guard_2(self) -> Cq.Workplane:
        return self.side_guard(
            attach_left=AttachPoint.DOVETAIL_OUT,
            attach_right=AttachPoint.DOVETAIL_IN,
        )
    @target(name="side_guard_3", angularTolerance=0.01)
    def side_guard_3(self) -> Cq.Workplane:
        return self.side_guard(
            attach_left=AttachPoint.DOVETAIL_OUT,
            attach_right=AttachPoint.DOVETAIL_IN,
        )
    @target(name="side_guard_4", angularTolerance=0.01)
    def side_guard_4(self) -> Cq.Workplane:
        return self.side_guard(
            attach_left=AttachPoint.DOVETAIL_OUT,
            attach_right=AttachPoint.SLOT,
        )

    def front_surrogate(self) -> Cq.Workplane:
        """
        Create a surrogate cylindrical section structure for the front since we
        cannot bend extrusions
        """
        angle = 360 / 5
        outer = Cq.Solid.makeCone(
            radius1=self.radius_lower_front + self.thickness,
            radius2=self.radius_upper_front + self.thickness,
            height=self.height,
            angleDegrees=angle,
        )
        inner = Cq.Solid.makeCone(
            radius1=self.radius_lower_front,
            radius2=self.radius_upper_front,
            height=self.height,
            angleDegrees=angle,
        )
        shell = (
            outer.cut(inner)
            .rotate((0,0,0), (0,0,1), -angle/2)
        )
        dx = math.sin(math.radians(angle / 2)) * self.radius_middle_front
        profile = (
            Cq.Workplane('YZ')
            .polyline([
                (0, self.height),
                (-dx, self.height / 2),
                (-dx, 0),
                (dx, 0),
                (dx, self.height / 2),
            ])
            .close()
            .extrude(self.radius_upper_front + self.side_guard_thickness)
            .val()
        )
        return shell * profile

    def assembly(self) -> Cq.Assembly:
        """
        New assembly using conformal mapping on the cone.
        """
        side_guards = [
            self.side_guard_1(),
            self.side_guard_2(),
            self.side_guard_3(),
            self.side_guard_4(),
        ]
        a = Cq.Assembly()
        for i,side_guard in enumerate(side_guards):
            angle = -(i+1) * 360 / self.facets
            a = a.addS(
                side_guard,
                name=f"side-{i}",
                material=self.material_side,
                loc=Cq.Location(rz=angle)
            )
        a.addS(
            self.front_surrogate(),
            name="front",
            material=self.material,
        )
        return a