"""
Profiles: shapes the primitives do not cover
============================================

Bricks, cylinders, cones and spheres carry most models, and Boolean
operations carry most of the rest.  Sooner or later a part resists both:
a curved electrode that is a slice of a tube, a pad with one rounded
end, a track that follows a route, a taper between two different
cross-sections.  Cutting those out of primitives means inventing tool
bodies whose only purpose is to be subtracted, and getting their
dimensions right by hand.

The way out is the one a draughtsman uses: draw the outline, then give
it a third dimension.  This tutorial builds that path — a curve, a
closed profile, a solid — and then the three verbs that operate on
whole shapes rather than outlines: hollowing, tracking, and lofting.

Nothing here runs a simulation; it is geometry only, and takes seconds.
"""

import math

import magnelio as mio
from magnelio import geo, plots

pec = mio.Material.pec()
copper = mio.Material.lossy_metal(name="copper", sigma=5.8e7)

# %%
# A worked example: a curved electrode
# ------------------------------------
#
# A 20 degree slice of a tube — 24 mm across, 2 mm wall, 30 mm tall.
# Two independent ways to build it, and it is worth seeing both, because
# the choice between them comes up again for every part.
#
# The first is a primitive.  A cylinder takes a bore and an angular
# extent, so the electrode is one call:

R_OUT, R_IN, HEIGHT, SPAN = 12.0e-3, 10.0e-3, 30.0e-3, 20.0

electrode = geo.Cylinder(
    radius=R_OUT,
    inner_radius=R_IN,
    height=HEIGHT,
    angle_deg=(0.0, SPAN),
    material=pec,
)

fig, ax = plots.plot_cross_section(
    [electrode], "z", HEIGHT / 2, title="electrode, cut across the axis"
)

# %%
# The same part, drawn as a profile
# ---------------------------------
#
# The second way draws the cross-section and extrudes it.  A
# :class:`~magnelio.geo.Path` is a pen: it starts at a point and
# remembers where the last segment ended, so each call only names where
# the segment goes.
#
# Two of the four sides are arcs about the axis.  An arc through a
# centre has two solutions — the short way round and the long way — so
# ``normal=`` names the axis the arc turns about, and the arc runs
# counter-clockwise about it.  That is the same sense as
# :meth:`~magnelio.geo.Shape.rotated`, and it settles the ambiguity even
# for a half-circle, where the two ends alone say nothing at all.

CENTRE = (0.0, 0.0, 0.0)


def on_circle(radius, angle_deg):
    """A point on a circle about the origin, in the z = 0 plane."""
    angle = math.radians(angle_deg)
    return (radius * math.cos(angle), radius * math.sin(angle), 0.0)


outline = (
    geo.Path(on_circle(R_IN, 0.0))
    .line_to(on_circle(R_OUT, 0.0))
    .arc_to(on_circle(R_OUT, SPAN), center=CENTRE, normal="z")
    .line_to(on_circle(R_IN, SPAN))
    .arc_to(on_circle(R_IN, 0.0), center=CENTRE, normal=(0.0, 0.0, -1.0))
    .closed()
)

drawn = outline.covered().extruded(vector=(0.0, 0.0, HEIGHT), material=pec)

# The two routes describe the same solid, and `volume()` is the way to
# say so: it reports what the CAD kernel actually built, not what the
# parameters nominally asked for.  They are built by different kernel
# operations, so they agree to numerical precision rather than bit for
# bit -- the level of agreement to expect whenever one part can be
# built two ways.
print(f"primitive: {electrode.volume() * 1e9:.6f} mm^3")
print(f"drawn:     {drawn.volume() * 1e9:.6f} mm^3")
print(f"relative difference: {abs(drawn.volume() / electrode.volume() - 1.0):.2e}")

# %%
# .. note::
#
#    The inner arc turns about ``-z`` while the outer turns about
#    ``+z``.  The pen walks the outline as a loop, so it comes back
#    along the inside — and "counter-clockwise" is then the other way
#    round.  If a profile comes out crossing itself, this is the first
#    thing to check.
#
# Which route to prefer?  The primitive, whenever it fits: it is one
# line, it carries its dimensions as named parameters, and the mesher
# gets an exact analytic surface.  The profile route earns its keep the
# moment the outline is not a plain sector — a broken edge, a keyway, a
# flat on one side.  Adding a chamfer to the inner corner is one more
# segment in the pen stroke, and no new tool body:

CHAMFER = 1.0e-3

chamfered = (
    geo.Path(on_circle(R_IN, 0.0))
    .line_to(on_circle(R_OUT - CHAMFER, 0.0))
    .line_to((*on_circle(R_OUT, 0.0)[:2], 0.0))
    .arc_to(on_circle(R_OUT, SPAN), center=CENTRE, normal="z")
    .line_to(on_circle(R_IN, SPAN))
    .arc_to(on_circle(R_IN, 0.0), center=CENTRE, normal=(0.0, 0.0, -1.0))
    .closed()
    .covered()
    .extruded(vector=(0.0, 0.0, HEIGHT), material=pec)
)

fig, ax = plots.plot_cross_section(
    [chamfered], "z", HEIGHT / 2, title="the same electrode, drawn and modified"
)

# %%
# A profile can also be revolved or swept
# ---------------------------------------
#
# The profile is the input, not the extrusion.  The same closed curve
# feeds three verbs: :meth:`~magnelio.geo.Shape.extruded` pushes it
# along a vector, :meth:`~magnelio.geo.Shape.revolved` turns it about an
# axis, and :meth:`~magnelio.geo.Shape.swept` runs it along a path.
#
# Revolving is the direct way to any rotationally symmetric part whose
# outline is not a rectangle — a rounded-nose centre conductor, a
# stepped transformer, a bead.  Here the outline is drawn in the x-z
# plane and turned about z:

nose = (
    geo.Path((0.0, 0.0, 0.0))
    .line_to((3.0e-3, 0.0, 0.0))
    .line_to((3.0e-3, 0.0, 8.0e-3))
    .arc_to((0.0, 0.0, 11.0e-3), via=(2.1e-3, 0.0, 10.1e-3))
    .closed()
    .covered()
    .revolved(axis="z", material=pec)
)

fig, ax = plots.plot_cross_section([nose], "y", 0.0, title="revolved profile: a rounded pin")

# %%
# Hollowing a solid
# -----------------
#
# A housing is a block with its inside removed, and the inside of
# anything but a box is not the outside scaled down.
# :meth:`~magnelio.geo.Shape.shelled` builds it directly: walls grow
# inward, so the outer dimensions stay exactly as designed, and naming a
# face leaves it out of the shell to become an opening.

WALL = 1.5e-3
BOX = (40.0e-3, 25.0e-3, 12.0e-3)

housing = geo.Brick(origin=(0.0, 0.0, 0.0), size=BOX, material=pec).shelled(
    thickness=WALL,
    opening_face_near=[(0.0, BOX[1] / 2, BOX[2] / 2), (BOX[0], BOX[1] / 2, BOX[2] / 2)],
)

fig, ax = plots.plot_cross_section([housing], "z", BOX[2] / 2, title="a housing, open at both ends")

# %%
# Tracks that follow a route
# --------------------------
#
# A feed line is a centreline with a width and a metal thickness.
# :meth:`~magnelio.geo.Curve.traced` takes it that way, so a bend is one
# segment of the path rather than a separate body to place.
#
# ``caps="flat"`` matters more than it looks: a track that ends at a
# port has to meet the port plane squarely, and the default rounded end
# would leave a sliver of air there.  Outside corners come out rounded,
# which is what a fabricated track does too.

W_TRACK, T_COPPER = 0.6e-3, 35.0e-6

route = (
    geo.Path((0.0, 0.0, 0.0))
    .line_to((8.0e-3, 0.0, 0.0))
    .spline_to((14.0e-3, 3.0e-3, 0.0), (20.0e-3, 3.0e-3, 0.0))
    .curve()
)

track = route.traced(
    width=W_TRACK,
    thickness=T_COPPER,
    caps="flat",
    normal="z",
    material=copper,
)

fig, ax = plots.plot_cross_section(
    [track], "z", T_COPPER / 2, title="a routed track, square at both ends"
)

# %%
# Tapers between two cross-sections
# ---------------------------------
#
# The last gap the primitives leave is a transition: a horn, a matching
# section, a change from a round cross-section to a rectangular one.
# :class:`~magnelio.geo.Loft` takes the cross-sections themselves, in
# the order the solid passes through them, and interpolates.
#
# ``blend="ruled"`` joins them with straight surfaces, which is what a
# machined taper is; the default ``blend="spline"`` passes one smooth
# surface through all of them, for a flared horn.
#
# Where the two ends are faces of solids that already exist, the
# :meth:`~magnelio.geo.Shape.lofted` verb takes those instead, and adds a
# third mode: ``blend="tangent"`` leaves each face along its own normal,
# so a bend between two parts that face different directions comes out
# smooth rather than creased.


def square(half, z):
    """A square outline of half-width *half*, at height *z*."""
    return geo.Face(
        normal="z",
        points=[(-half, -half), (half, -half), (half, half), (-half, half)],
        position=z,
    )


taper = geo.Loft(square(4.0e-3, 0.0), square(10.0e-3, 18.0e-3), blend="ruled", material=pec)

fig, ax = plots.plot_cross_section([taper], "y", 0.0, title="a ruled taper between two squares")

# %%
# What to take away
# -----------------
#
# * Reach for a primitive first — a cylinder with ``inner_radius`` and
#   ``angle_deg`` covers tubes and sectors without any drawing.
# * When the outline is the thing you actually know, draw it with
#   :class:`~magnelio.geo.Path`, close it, and cover it.  The resulting
#   sheet is a profile for ``extruded`` / ``revolved`` / ``swept`` /
#   ``thickened``.
# * An arc through a centre is ambiguous; ``normal=`` removes the
#   ambiguity, and the direction reverses when the pen comes back along
#   the far side of a loop.
# * ``shelled`` hollows, ``traced`` follows a route, ``Loft``
#   interpolates cross-sections.  Each replaces a construction that
#   would otherwise be assembled from tool bodies by hand.
# * :meth:`~magnelio.geo.Shape.volume` checks a construction against
#   what it was supposed to be — the metal fraction of a housing, or
#   two routes to the same part agreeing.
#
# A profile carrying no material is a *construction* profile: it is not
# a physical object and cannot be meshed on its own, which is why the
# verbs that turn it into a solid ask for the material explicitly.
