<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Narrow Phase (SAT) | ACCB5017</title>
  <link rel="icon" type="image/svg+xml" href="https://www.uwtsd.ac.uk/themes/custom/rutherford/images/logo.svg">

  <style>
    code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
  </style>

  <style>
    /* ---- Reset and base ---- */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      --uwtsd-navy:    #003366;
      --uwtsd-gold:    #C8A951;
      --uwtsd-light:   #F4F4F4;
      --uwtsd-white:   #FFFFFF;
      --uwtsd-text:    #1A1A1A;
      --uwtsd-muted:   #555555;
      --uwtsd-code-bg: #1E1E1E;
    }

    body {
      font-family: 'Segoe UI', Arial, sans-serif;
      font-size: 16px;
      line-height: 1.6;
      color: var(--uwtsd-text);
      background: var(--uwtsd-light);
    }

    /* ---- Header ---- */
    .lab-header {
      background: var(--uwtsd-navy);
      color: var(--uwtsd-white);
      padding: 1.25rem 2rem;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }

    .lab-header img {
      height: 48px;
      filter: brightness(0) invert(1);
    }

    .lab-header-text { flex: 1; }

    .lab-header-text .module-code {
      font-size: 0.8rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--uwtsd-gold);
      font-weight: 600;
    }

    .lab-header-text h1 {
      font-size: 1.4rem;
      font-weight: 700;
      margin-top: 0.2rem;
    }

    /* ---- Main content ---- */
    main {
      max-width: 960px;
      margin: 2rem auto;
      padding: 0 1.5rem;
    }

    /* ---- Sections ---- */
    .lab-section {
      background: var(--uwtsd-white);
      border-radius: 6px;
      padding: 1.5rem 1.75rem;
      margin-bottom: 1.5rem;
      box-shadow: 0 1px 4px rgba(0,0,0,0.07);
    }

    .lab-section h2 {
      font-size: 1.1rem;
      font-weight: 700;
      color: var(--uwtsd-navy);
      margin-bottom: 1rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
    }

    .lab-section h3 {
      font-size: 1rem;
      font-weight: 600;
      color: var(--uwtsd-navy);
      margin: 1.25rem 0 0.5rem;
    }

    .lab-section p { margin-bottom: 0.75rem; }

    .lab-section ul,
    .lab-section ol {
      padding-left: 1.4rem;
      margin-bottom: 0.75rem;
    }

    .lab-section li { margin-bottom: 0.35rem; }

    code {
      font-family: 'Consolas', 'Courier New', monospace;
      font-size: 0.88em;
      background: #eef0f4;
      color: #c0392b;
      padding: 0.1em 0.35em;
      border-radius: 3px;
    }

    kbd {
      font-family: 'Consolas', 'Courier New', monospace;
      font-size: 0.85em;
      background: #f0f0f0;
      border: 1px solid #ccc;
      border-radius: 3px;
      padding: 0.1em 0.4em;
    }

    /* ---- Code blocks ---- */
    pre {
      background: var(--uwtsd-code-bg) !important;
      color: #cccccc;
      border-radius: 6px;
      padding: 1.1rem 1.25rem;
      overflow-x: auto;
      margin: 0.75rem 0 1rem;
      font-size: 0.88rem;
      line-height: 1.5;
    }

    pre code {
      background: none !important;
      color: inherit !important;
      padding: 0;
      font-size: inherit;
    }

    /* ---- Package table ---- */
    .pkg-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 0.93rem;
      margin-top: 0.5rem;
    }

    .pkg-table th {
      background: var(--uwtsd-navy);
      color: var(--uwtsd-white);
      text-align: left;
      padding: 0.6rem 0.85rem;
      font-size: 0.8rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }

    .pkg-table td {
      padding: 0.7rem 0.85rem;
      vertical-align: top;
      border-bottom: 1px solid #e4e8f0;
    }

    .pkg-table td:first-child { white-space: nowrap; }
    .pkg-table td:last-child  { white-space: nowrap; }

    .pkg-table tr:last-child td { border-bottom: none; }

    .pkg-table tr:nth-child(even) td { background: #f8f9fc; }

    .pkg-table a { color: var(--uwtsd-navy); font-weight: 600; }
    .pkg-table a:hover { text-decoration: underline; }

    /* ---- Platform tabs ---- */
    .tab-bar {
      display: flex;
      gap: 0.25rem;
      margin-bottom: 1.25rem;
      border-bottom: 2px solid #dde3ed;
    }

    .tab-btn {
      padding: 0.5rem 1.1rem;
      font-size: 0.88rem;
      font-weight: 600;
      font-family: inherit;
      border: none;
      background: none;
      color: var(--uwtsd-muted);
      cursor: pointer;
      border-bottom: 3px solid transparent;
      margin-bottom: -2px;
      transition: color 0.15s, border-color 0.15s;
    }

    .tab-btn:hover { color: var(--uwtsd-navy); }

    .tab-btn.active {
      color: var(--uwtsd-navy);
      border-bottom-color: var(--uwtsd-gold);
    }

    .tab-panel { display: none; }
    .tab-panel.active { display: block; }

    /* ---- Task steps ---- */
    .task-block {
      border: 1px solid #dde3ed;
      border-radius: 6px;
      padding: 1rem 1.25rem;
      margin: 1rem 0;
      background: #f8f9fc;
    }

    .task-block h3 {
      font-size: 0.95rem;
      color: var(--uwtsd-navy);
      margin-bottom: 0.5rem;
    }

    /* ---- Code file header ---- */
    .code-file-header {
      background: #2d2d2d;
      color: #888;
      font-family: 'Consolas', 'Courier New', monospace;
      font-size: 0.78rem;
      padding: 0.3rem 1.25rem;
      border-radius: 6px 6px 0 0;
      letter-spacing: 0.03em;
    }

    .code-file-header + pre {
      border-radius: 0 0 6px 6px !important;
      margin-top: 0 !important;
    }

    /* ---- Solution reveal ---- */
    .solution-btn {
      display: inline-block;
      margin: 0.5rem 0 1rem;
      padding: 0.55rem 1.4rem;
      background: var(--uwtsd-navy);
      color: var(--uwtsd-white);
      border: none;
      border-radius: 4px;
      font-size: 0.93rem;
      font-weight: 600;
      font-family: inherit;
      cursor: pointer;
      transition: background 0.15s;
    }

    .solution-btn:hover { background: #004080; }

    .solution-panel { display: none; margin-top: 0.5rem; }
    .solution-panel.open { display: block; }

    .solution-canvas-wrap {
      overflow-x: auto;
      margin-bottom: 1rem;
    }
    .solution-canvas-wrap:last-child { margin-bottom: 0; }

    /* ---- Fill-in-the-blank ---- */
    .fill-blanks-group { margin: 0.5rem 0 0.25rem; }

    .blank-question {
      margin: 0.6rem 0;
      line-height: 2.2;
    }

    .blank-input {
      display: inline-block;
      border: none;
      border-bottom: 2px solid var(--uwtsd-navy);
      background: transparent;
      font-family: inherit;
      font-size: inherit;
      color: var(--uwtsd-text);
      padding: 0.05em 0.3em;
      width: 14em;
      outline: none;
      transition: border-color 0.2s, background 0.2s;
      vertical-align: baseline;
    }

    .blank-input:focus { border-bottom-color: var(--uwtsd-gold); }

    .blank-input.blank-correct {
      border-bottom-color: #27ae60;
      background: rgba(39,174,96,0.09);
      border-radius: 3px 3px 0 0;
    }

    .blank-input.blank-incorrect {
      border-bottom-color: #e74c3c;
      background: rgba(231,76,60,0.09);
      border-radius: 3px 3px 0 0;
    }

    .blank-feedback {
      display: block;
      font-size: 0.82rem;
      font-weight: 600;
      margin-top: 0.2rem;
      min-height: 1.2em;
    }

    .feedback-correct { color: #27ae60; }
    .feedback-incorrect { color: #c0392b; }

    .check-blanks-btn {
      display: inline-block;
      margin: 0.75rem 0 0.25rem;
      padding: 0.5rem 1.2rem;
      background: var(--uwtsd-gold);
      color: var(--uwtsd-navy);
      border: none;
      border-radius: 4px;
      font-size: 0.9rem;
      font-weight: 700;
      font-family: inherit;
      cursor: pointer;
      transition: background 0.15s;
    }

    .check-blanks-btn:hover { background: #b8952e; }

    /* ---- Multiple choice ---- */
    .mc-block { margin: 0.75rem 0 1rem; }

    .mc-item {
      border: 1px solid #dde3ed;
      border-radius: 6px;
      padding: 0.75rem 1rem;
      margin-bottom: 0.75rem;
      background: #fff;
    }

    .mc-question-text { font-weight: 600; margin-bottom: 0.5rem; }

    .mc-options { display: flex; flex-direction: column; gap: 0; margin: 0.4rem 0; }

    .mc-option {
      display: flex;
      align-items: flex-start;
      gap: 0.6rem;
      padding: 0.35rem 0.5rem;
      cursor: pointer;
      border-radius: 4px;
      transition: background 0.1s;
    }
    .mc-option:hover { background: #f0f4ff; }

    .mc-option input[type=radio] { margin-top: 0.2em; flex-shrink: 0; }

    .mc-option.mc-correct   { color: #27ae60; font-weight: 600; }
    .mc-option.mc-incorrect { color: #e74c3c; }

    .mc-explanation {
      display: none;
      font-size: 0.88rem;
      color: var(--uwtsd-muted);
      margin-top: 0.4rem;
      padding: 0.4rem 0.6rem;
      border-left: 3px solid var(--uwtsd-gold);
      background: #fffdf0;
    }
    .mc-explanation.visible { display: block; }

    .mc-feedback {
      font-size: 0.82rem;
      font-weight: 600;
      min-height: 1.2em;
      margin-top: 0.25rem;
    }

    .mc-item-static { margin-bottom: 0.75rem; }
    .mc-options-static { padding-left: 1.4rem; margin: 0.4rem 0; }

    /* ---- Short answer ---- */
    .sa-block { margin: 0.75rem 0 1rem; }

    .sa-item {
      border: 1px solid #dde3ed;
      border-radius: 6px;
      padding: 0.75rem 1rem;
      margin-bottom: 0.75rem;
      background: #fff;
    }

    .sa-question-text { font-weight: 600; margin-bottom: 0.5rem; }

    .sa-textarea {
      width: 100%;
      min-height: 6rem;
      padding: 0.5rem 0.75rem;
      font-family: inherit;
      font-size: 0.93rem;
      border: 1px solid #ccd0da;
      border-radius: 4px;
      resize: vertical;
      margin-bottom: 0.5rem;
      color: var(--uwtsd-text);
      background: #fafbfc;
      box-sizing: border-box;
    }
    .sa-textarea:focus { outline: none; border-color: var(--uwtsd-navy); }

    .sa-reveal-btn {
      display: inline-block;
      padding: 0.4rem 1rem;
      background: transparent;
      color: var(--uwtsd-navy);
      border: 1px solid var(--uwtsd-navy);
      border-radius: 4px;
      font-size: 0.88rem;
      font-weight: 600;
      font-family: inherit;
      cursor: pointer;
      transition: background 0.15s, color 0.15s;
    }
    .sa-reveal-btn:hover { background: var(--uwtsd-navy); color: white; }

    .sa-answer-panel {
      display: none;
      margin-top: 0.5rem;
      padding: 0.6rem 0.85rem;
      background: #f0f4f0;
      border-left: 3px solid #27ae60;
      border-radius: 0 4px 4px 0;
      font-size: 0.93rem;
    }
    .sa-answer-panel.visible { display: block; }

    .sa-item-static { margin-bottom: 0.75rem; }
    .sa-lines {
      border-bottom: 1px solid #aaa;
      min-height: 5rem;
      margin-top: 0.4rem;
    }

    /* ---- Math blocks (KaTeX pre-rendered) ---- */
    .math-block {
      margin: 1rem 0;
      overflow-x: auto;
      text-align: center;
    }
    .math-block .katex-display { margin: 0; }

    /* ---- Visualization blocks (D3) ---- */
    .viz-block {
      margin: 1rem 0;
      border: 1px solid #dde3ed;
      border-radius: 6px;
      overflow: hidden;
      background: #fff;
    }

    /* ---- Print stylesheet ---- */
    @media print {
      .lab-header { background: white; color: black; border-bottom: 2px solid #003366; }
      .lab-header img { filter: none; }
      .lab-header-text .module-code { color: #003366; }
      .lab-header-text .lab-meta { color: #555; }
      body { background: white; font-size: 13px; }
      main { max-width: 100%; margin: 0; padding: 0 1rem; }
      .lab-section { box-shadow: none; border: 1px solid #ccc; break-inside: avoid; }
      pre { white-space: pre-wrap; word-break: break-word; }
      a { color: inherit; text-decoration: none; }
      .solution-btn { display: none; }
      .solution-panel { display: block !important; }
      .check-blanks-btn { display: none; }
      .blank-input { border-bottom: 1px solid #555; min-width: 8em; background: transparent !important; }
      .blank-feedback { display: none; }
      .mc-option { break-inside: avoid; }
      .mc-option input[type=radio] { display: none; }
      .sa-reveal-btn { display: none; }
      .sa-textarea { border: 1px solid #999; background: transparent !important; min-height: 5rem; }
      .sa-answer-panel { display: block !important; border-left: 2px solid #555; background: #f9f9f9; }
      @page {
        margin: 1.8cm 2cm;
        @top-right { content: "ACCB5017"; font-size: 9pt; color: #555; }
        @bottom-center { content: counter(page) " of " counter(pages); font-size: 9pt; }
      }
    }
  </style>
</head>
<body>

<header class="lab-header">
  <img src="https://www.uwtsd.ac.uk/themes/custom/rutherford/images/logo.svg"
       alt="University of Wales Trinity Saint David">
  <div class="lab-header-text">
    <div class="module-code">ACCB5017 - Games Architecture and Mechanics</div>
    <h1>Narrow Phase (SAT)</h1>
  </div>
</header>

<main>

  <section class="lab-section">
    <h2>Overview</h2>
    <p>This lab builds a small SFML project that answers a precise question a cheap overlap check cannot: given two candidate shapes, are they actually touching, and if so, by how much and in which direction? Working from an <code>OBB</code> class that spins and drifts around a window, you will build a broad-phase pre-check that flags candidate pairs from their bounding boxes, then a narrow-phase <code>SAT</code> test using the Separating Axis Theorem to confirm or reject each candidate against the shapes' true, rotated corners. By the end, a live window shows a user-controlled box alongside several auto-rotating ones, drawing a thin yellow line between any pair whose bounding boxes overlap but whose true shapes do not touch, a thick red line and a push-direction arrow between any pair SAT confirms are genuinely colliding, and on-screen counts of candidates, confirmed collisions and false positives, updating live as boxes spin and drift into and out of each other.</p>
  </section>

  <section class="lab-section">
    <h2>Learning Objectives</h2>
    <p>By the end of this lab you will be able to:</p>
    <ul>
      <li>Explain why a rotating box's tight axis-aligned bounding box is not a fixed size, and why that makes an AABB overlap test alone unreliable for rotated shapes</li>
      <li>Implement the Separating Axis Theorem's projection test, checking a pair of oriented boxes against each box's own face-normal axes and exiting early the moment one axis proves separation</li>
      <li>Compute the Minimum Translation Vector for a confirmed collision, the axis and distance that would push the two shapes apart by the smallest amount</li>
      <li>Distinguish a cheap broad-phase candidate check from a precise narrow-phase confirmation, and observe on screen how often the cheap check alone gets the answer wrong</li>
    </ul>
  </section>

  <section class="lab-section">
    <h2>Concept</h2>
    <p>A bounding box built from a shape's rotated corners is only ever an approximation. Two boxes might have overlapping bounding boxes while their true, rotated outlines do not touch at all, a <strong>broad-phase false positive</strong>. The bigger the rotation, the worse this gets. At 0 or 90 degrees a box's <strong>axis-aligned bounding box</strong> (AABB) matches its true outline exactly, but as it rotates toward 45 degrees the AABB has to grow outward to still contain every corner, swelling toward the box's full half-diagonal before shrinking back down as the rotation continues past 45. A cheap AABB overlap test alone cannot tell a genuine collision from two boxes that merely have swollen, overlapping bounds.</p>

<p>The <strong>Separating Axis Theorem</strong> (SAT) resolves this precisely for convex shapes. Two convex shapes are not overlapping if and only if there exists at least one axis onto which their projections, their shadows cast along that axis, do not overlap. That axis is called a <strong>separating axis</strong>. For two boxes, only each box's own face-normal directions need testing, two per box, four in total for a pair, since a rectangle has only two unique edge directions. The test projects both boxes' corners onto each candidate axis in turn using a dot product, and compares the resulting min/max ranges. The moment any single axis shows a gap between the two projected ranges, that axis alone proves the boxes cannot be touching, and the test can stop immediately without checking the rest. Only if every one of the four axes shows overlapping projections are the boxes provably colliding.</p>

<p>When a pair is confirmed colliding, the axis with the smallest overlap is the <strong>Minimum Translation Vector</strong> (MTV), the cheapest single direction and distance that would separate the two shapes. This lab represents an SAT result as a small <code>Result</code> struct holding a <code>bool colliding</code>, a unit-length <code>normal</code> pointing from one box's centre toward the other's, and a <code>penetration</code> distance along that normal. The projection step itself is a <code>dot</code> product between a corner and an axis, tracking a running <code>min</code> and <code>max</code> across all four corners of one box for a given axis.</p>
  </section>

  <section class="lab-section">
    <h2>Tasks</h2>

    <div class="task-block">
      <h3>Task 1 - A Box That Spins, Drifts and Bounces</h3>
      
<p>Create a new <strong>Console App (C++)</strong> project in Visual Studio the same way as in the Environment Setup lab, naming it <code>NarrowPhase</code>. Use <strong>Add &gt; New Item</strong> to add a new header file named <code>OBB.h</code>, and write the class shown below. An <code>OBB</code> (oriented bounding box) is a rotated rectangle stored as a centre, half-extents, and a rotation angle in radians, together with an angular and linear velocity so it can spin and drift on its own. <code>update</code> advances the rotation and position for one frame and bounces off the window's edges using the box's half-diagonal as a simple circular bound, so the box never visibly pokes through a wall no matter its rotation. <code>getCorners</code> returns the four true, rotated corners in world space, and <code>draw</code> renders exactly that same corner data as an outlined polygon, so the shape on screen always matches the shape any later test will examine.</p>
<p><strong>Success condition:</strong> the project compiles with <code>OBB</code> fully defined in <code>OBB.h</code>. There is no window yet, that is added in the next task.</p>


<div class="code-file-header">OBB.h</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">#pragma once
#include &lt;SFML/Graphics.hpp&gt;
#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;cmath&gt;

class OBB {
public:
    OBB(sf::Vector2f centerPos, sf::Vector2f halfExtents, float rotationRadians,
        float angularVelocityRadPerSec, sf::Vector2f linearVelocity, sf::Color colour)
        : center(centerPos), halfExtents(halfExtents), rotation(rotationRadians),
          angularVelocity(angularVelocityRadPerSec), velocity(linearVelocity), colour(colour) {}

    void update(float deltaTime, float worldWidth, float worldHeight) {
        rotation += angularVelocity * deltaTime;
        center += velocity * deltaTime;

        float boundRadius = boundingRadius();

        if (center.x - boundRadius &lt; 0.0f) {
            center.x = boundRadius;
            velocity.x = -velocity.x;
        } else if (center.x + boundRadius &gt; worldWidth) {
            center.x = worldWidth - boundRadius;
            velocity.x = -velocity.x;
        }

        if (center.y - boundRadius &lt; 0.0f) {
            center.y = boundRadius;
            velocity.y = -velocity.y;
        } else if (center.y + boundRadius &gt; worldHeight) {
            center.y = worldHeight - boundRadius;
            velocity.y = -velocity.y;
        }
    }

    std::array&lt;sf::Vector2f, 4&gt; getCorners() const {
        float cosR = std::cos(rotation);
        float sinR = std::sin(rotation);

        std::array&lt;sf::Vector2f, 4&gt; localSigns = {
            sf::Vector2f(-halfExtents.x, -halfExtents.y),
            sf::Vector2f(halfExtents.x, -halfExtents.y),
            sf::Vector2f(halfExtents.x, halfExtents.y),
            sf::Vector2f(-halfExtents.x, halfExtents.y)
        };

        std::array&lt;sf::Vector2f, 4&gt; corners;
        for (std::size_t i = 0; i &lt; 4; ++i) {
            float localX = localSigns[i].x;
            float localY = localSigns[i].y;
            corners[i] = sf::Vector2f(
                center.x + localX * cosR - localY * sinR,
                center.y + localX * sinR + localY * cosR);
        }
        return corners;
    }

    std::array&lt;sf::Vector2f, 2&gt; getAxes() const {
        float cosR = std::cos(rotation);
        float sinR = std::sin(rotation);
        return { sf::Vector2f(cosR, sinR), sf::Vector2f(-sinR, cosR) };
    }

    sf::FloatRect getAABB() const {
        auto corners = getCorners();
        float minX = corners[0].x, maxX = corners[0].x;
        float minY = corners[0].y, maxY = corners[0].y;
        for (std::size_t i = 1; i &lt; 4; ++i) {
            minX = std::min(minX, corners[i].x);
            maxX = std::max(maxX, corners[i].x);
            minY = std::min(minY, corners[i].y);
            maxY = std::max(maxY, corners[i].y);
        }
        return sf::FloatRect(sf::Vector2f(minX, minY), sf::Vector2f(maxX - minX, maxY - minY));
    }

    float boundingRadius() const {
        return std::sqrt(halfExtents.x * halfExtents.x + halfExtents.y * halfExtents.y);
    }

    void draw(sf::RenderWindow&amp; window, sf::Color outlineColour, float outlineThickness = 2.0f) const {
        auto corners = getCorners();
        sf::ConvexShape shape(4);
        for (std::size_t i = 0; i &lt; 4; ++i) {
            shape.setPoint(i, corners[i]);
        }
        shape.setFillColor(sf::Color::Transparent);
        shape.setOutlineColor(outlineColour);
        shape.setOutlineThickness(outlineThickness);
        window.draw(shape);
    }

    void drawAABB(sf::RenderWindow&amp; window, sf::Color colour) const {
        sf::FloatRect bounds = getAABB();
        sf::RectangleShape box(bounds.size);
        box.setPosition(bounds.position);
        box.setFillColor(sf::Color::Transparent);
        box.setOutlineColor(colour);
        box.setOutlineThickness(1.0f);
        window.draw(box);
    }

    sf::Vector2f center;
    sf::Vector2f halfExtents;
    float rotation;
    float angularVelocity;
    sf::Vector2f velocity;
    sf::Color colour;
};</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 2 - A Window With One Box You Control</h3>
      
<p>Use <strong>Add &gt; New Item</strong> to add a new C++ source file named <code>NarrowPhaseDemo.cpp</code>, and write the <code>main</code> function shown below. It opens a window and creates a single green <code>OBB</code> in the centre, moved with the arrow keys or <code>WASD</code> and rotated with <code>Q</code> and <code>E</code>, using continuous key state rather than key-press events so holding a key moves or rotates it smoothly frame to frame.</p>
<p><strong>Success condition:</strong> a window opens showing one green box in the centre. Arrow keys or <code>WASD</code> move it, <code>Q</code> and <code>E</code> rotate it, and it stays fully on screen.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">#include &lt;SFML/Graphics.hpp&gt;
#include &lt;algorithm&gt;
#include &lt;optional&gt;
#include &quot;OBB.h&quot;

namespace {
    constexpr unsigned int WORLD_WIDTH = 900;
    constexpr unsigned int WORLD_HEIGHT = 800;
    constexpr float MOVE_SPEED = 220.0f;
    constexpr float ROTATE_SPEED = 1.6f;
}

int main() {
    sf::VideoMode videoMode(sf::Vector2u(WORLD_WIDTH, WORLD_HEIGHT));
    sf::RenderWindow window(videoMode, &quot;Narrow Phase - SAT on Rotating Boxes&quot;);
    window.setFramerateLimit(60);

    OBB userBox(
        sf::Vector2f(WORLD_WIDTH * 0.5f, WORLD_HEIGHT * 0.5f),
        sf::Vector2f(35.0f, 35.0f),
        0.0f, 0.0f,
        sf::Vector2f(0.0f, 0.0f),
        sf::Color(80, 220, 140));

    sf::Clock clock;

    while (window.isOpen()) {
        float deltaTime = clock.restart().asSeconds();

        while (const std::optional event = window.pollEvent()) {
            if (event-&gt;is&lt;sf::Event::Closed&gt;()) {
                window.close();
            }
        }

        sf::Vector2f move(0.0f, 0.0f);
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Left) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::A)) {
            move.x -= 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Right) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::D)) {
            move.x += 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Up) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::W)) {
            move.y -= 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Down) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::S)) {
            move.y += 1.0f;
        }
        userBox.center += move * MOVE_SPEED * deltaTime;
        userBox.center.x = std::clamp(userBox.center.x, 0.0f, static_cast&lt;float&gt;(WORLD_WIDTH));
        userBox.center.y = std::clamp(userBox.center.y, 0.0f, static_cast&lt;float&gt;(WORLD_HEIGHT));

        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Q)) {
            userBox.rotation -= ROTATE_SPEED * deltaTime;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::E)) {
            userBox.rotation += ROTATE_SPEED * deltaTime;
        }

        window.clear(sf::Color(12, 12, 18));
        userBox.draw(window, userBox.colour, 3.0f);
        window.display();
    }

    return 0;
}</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 3 - Filling the Scene With Auto-Rotating Boxes</h3>
      
<p>Open <code>NarrowPhaseDemo.cpp</code>. Add a helper that builds a randomly placed, randomly spinning box, and use it to fill the scene with several boxes alongside the user's green one.</p>
<ol>
  <li>In the anonymous namespace, add a <code>std::mt19937 rng(std::random_device{}())</code> and a <code>makeRandomBox(float worldWidth, float worldHeight)</code> function returning an <code>OBB</code> with a random position, a half-extent between <code>20.0f</code> and <code>45.0f</code>, a random starting rotation, a random angular velocity between <code>0.25f</code> and <code>1.6f</code> radians per second (positive or negative), a random colour, and a linear velocity that is <code>(0, 0)</code> for roughly half the boxes and a small random drift for the rest.</li>
  <li>In <code>main</code>, declare a <code>std::vector&lt;OBB&gt; boxes</code>, push <code>userBox</code> onto it first so it stays index <code>0</code>, then push seven more boxes built from <code>makeRandomBox</code>.</li>
  <li>Replace the single <code>userBox.center += move * ...</code> block so it only ever touches <code>boxes[0]</code>, and add a loop calling <code>update</code> on every box from index <code>1</code> onward.</li>
  <li>Replace the single <code>userBox.draw(...)</code> call with a loop drawing every box in <code>boxes</code>, each with <code>box.draw(window, box.colour, 2.0f)</code>.</li>
</ol>
<p><strong>Success condition:</strong> the window shows the green user box plus several other boxes spinning and drifting independently, all bouncing off the window's edges.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">namespace {
    constexpr unsigned int WORLD_WIDTH = 900;
    constexpr unsigned int WORLD_HEIGHT = 800;
    constexpr std::size_t BOX_COUNT = 8; // includes the user-controlled box

    constexpr float MOVE_SPEED = 220.0f;
    constexpr float ROTATE_SPEED = 1.6f;

    // TODO: declare std::mt19937 rng(std::random_device{}())

    // TODO: sf::Color randomColour() using a std::uniform_int_distribution&lt;int&gt; c(100, 255)

    // TODO: OBB makeRandomBox(float worldWidth, float worldHeight), returning an
    // OBB with a random position (60 px margin from the edges), a random
    // half-extent between 20.0f and 45.0f, a random rotation, a random
    // angular velocity between 0.25f and 1.6f rad/s (flip its sign with a
    // std::bernoulli_distribution), a random colour, and velocity (0, 0)
    // for roughly half the boxes and a small random drift for the rest
}

// In main(): declare std::vector&lt;OBB&gt; boxes, push userBox first, then push
// seven more from makeRandomBox. Update boxes[1] onward each frame and draw
// every box in the vector.</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 4 - Watching the Bounding Box Swell and Shrink</h3>
      
<p>Open <code>NarrowPhaseDemo.cpp</code>. Add a toggle so every box's tight bounding box can be shown alongside its true rotated outline, making the problem this lab exists to solve visible directly.</p>
<ol>
  <li>Declare <code>bool showAABB = true;</code> before the main loop.</li>
  <li>Inside the event-polling loop, check for an <code>sf::Event::KeyPressed</code> event with <code>event-&gt;getIf&lt;sf::Event::KeyPressed&gt;()</code>, and flip <code>showAABB</code> when <code>keyPressed-&gt;scancode</code> is <code>sf::Keyboard::Scancode::G</code>.</li>
  <li>Just before drawing the boxes themselves, if <code>showAABB</code> is true, loop over every box and call <code>box.drawAABB(window, sf::Color(90, 90, 110, 130))</code>.</li>
</ol>
<p><strong>Success condition:</strong> pressing <code>G</code> toggles a translucent rectangle around every box showing its tight axis-aligned bounds. Watch one spinning box closely: its bounding box visibly grows as the box rotates toward 45 degrees and shrinks back down as it continues rotating toward 90.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">// Before the main loop:
bool showAABB = true;

// Inside the event-polling loop, alongside the existing sf::Event::Closed check:
if (const auto* keyPressed = event-&gt;getIf&lt;sf::Event::KeyPressed&gt;()) {
    switch (keyPressed-&gt;scancode) {
        // TODO: on sf::Keyboard::Scancode::G, flip showAABB
        default:
            break;
    }
}

// Just before the loop that draws every box:
// TODO: if showAABB, loop over every box and call
// box.drawAABB(window, sf::Color(90, 90, 110, 130))</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 5 - Declaring the SAT Module</h3>
      
<p>In Solution Explorer, use <strong>Add &gt; New Item</strong> to add a new header file named <code>SAT.h</code>, and write the declarations shown below. A <code>Result</code> struct carries everything a caller needs back from one test: whether the pair collides, the unit-length push direction, and how far apart they overlap along that direction. The two free functions inside the <code>SAT</code> namespace are declared here but not yet defined, that happens over the next two tasks.</p>
<p><strong>Success condition:</strong> the project still compiles, nothing calls these functions yet.</p>


<div class="code-file-header">SAT.h</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">#pragma once
#include &lt;SFML/Graphics.hpp&gt;
#include &quot;OBB.h&quot;

namespace SAT {
    struct Result {
        bool colliding = false;
        sf::Vector2f normal{ 0.f, 0.f }; // unit MTV direction, points from A's centre toward B's
        float penetration = 0.f;          // MTV magnitude along normal; 0 if not colliding
    };

    // Precise narrow-phase test between two oriented boxes.
    Result test(const OBB&amp; a, const OBB&amp; b);

    // Cheap broad-phase pre-check: tight AABB vs tight AABB.
    bool aabbOverlap(const OBB&amp; a, const OBB&amp; b);
}</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 6 - Implementing the Broad-Phase Pre-Check</h3>
      
<p>Use <strong>Add &gt; New Item</strong> to add a new C++ source file named <code>SAT.cpp</code>, and implement <code>aabbOverlap</code> shown below. This is a cheap first pass, exactly the tight AABB drawn by the <code>G</code> toggle in the previous task, used here to decide whether a pair is even worth testing precisely.</p>
<ol>
  <li>Include <code>"SAT.h"</code>.</li>
  <li>Define <code>SAT::aabbOverlap</code> to return <code>a.getAABB().findIntersection(b.getAABB()).has_value()</code>.</li>
</ol>
<p><strong>Success condition:</strong> the project compiles with <code>aabbOverlap</code> defined. <code>SAT::test</code> is declared in <code>SAT.h</code> but still has no definition, that is fine as long as nothing calls it yet.</p>


<div class="code-file-header">SAT.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">#include &quot;SAT.h&quot;

// TODO: define bool SAT::aabbOverlap(const OBB&amp; a, const OBB&amp; b), returning
// a.getAABB().findIntersection(b.getAABB()).has_value()</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 7 - Finding and Drawing Candidate Pairs</h3>
      
<p>Open <code>NarrowPhaseDemo.cpp</code>. Each frame, build the list of candidate pairs using the same brute-force, every-pair-once approach as the Brute-Force Broad Phase lab, this time filtering with <code>SAT::aabbOverlap</code> instead of a circle overlap test, then draw a thin line between the centres of every candidate pair so the broad-phase result is visible on screen before any precise test runs.</p>
<ol>
  <li>Include <code>"SAT.h"</code> alongside the existing includes.</li>
  <li>Each frame, after updating every box, declare <code>std::vector&lt;std::pair&lt;std::size_t, std::size_t&gt;&gt; candidatePairs</code>, and fill it with a nested loop over every <code>i &lt; boxes.size()</code> and <code>j</code> from <code>i + 1</code> to <code>boxes.size() - 1</code>, pushing <code>(i, j)</code> whenever <code>SAT::aabbOverlap(boxes[i], boxes[j])</code> is true.</li>
  <li>After drawing the AABB overlay and before drawing the boxes themselves, loop over <code>candidatePairs</code> and draw a line between <code>boxes[pair.first].center</code> and <code>boxes[pair.second].center</code> using two <code>sf::Vertex</code> entries coloured <code>sf::Color(255, 220, 60, 140)</code> and <code>sf::PrimitiveType::Lines</code>.</li>
  <li>Build an <code>sf::Text</code> reporting <code>boxes.size()</code> and <code>candidatePairs.size()</code>, and draw it in the top-left corner.</li>
</ol>
<p><strong>Success condition:</strong> a yellow line connects every pair of boxes whose bounding boxes overlap. Toggle <code>G</code> on and watch closely: some yellow lines connect boxes whose true rotated outlines are clearly not touching, only their swollen bounding boxes are overlapping. That gap is exactly what the next task closes.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">// After updating every box, before clearing the window:
// TODO: declare std::vector&lt;std::pair&lt;std::size_t, std::size_t&gt;&gt; candidatePairs
// and fill it with a nested loop (i from 0, j from i + 1) pushing (i, j)
// whenever SAT::aabbOverlap(boxes[i], boxes[j]) is true

// After drawing the AABB overlay, before drawing the boxes:
// TODO: loop over candidatePairs and draw a line between
// boxes[pair.first].center and boxes[pair.second].center using two
// sf::Vertex entries coloured sf::Color(255, 220, 60, 140) and
// sf::PrimitiveType::Lines

// TODO: build and draw an sf::Text in the top-left corner reporting
// boxes.size() and candidatePairs.size()</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 8 - Projecting a Box Onto an Axis</h3>
      
<p>Open <code>SAT.cpp</code> and add the two small helpers shown below, in an anonymous namespace above <code>aabbOverlap</code>. Every later step of the SAT test relies on these to turn a box's four corners into a single range along one axis.</p>
<ol>
  <li>Add a <code>struct Projection { float min; float max; }</code>.</li>
  <li>Add <code>float dot(sf::Vector2f a, sf::Vector2f b)</code> returning <code>a.x * b.x + a.y * b.y</code>.</li>
  <li>Add <code>Projection project(const std::array&lt;sf::Vector2f, 4&gt;&amp; corners, sf::Vector2f axis)</code>. Start <code>mn</code> and <code>mx</code> both at <code>dot(corners[0], axis)</code>, then loop <code>i</code> from <code>1</code> to <code>3</code>, computing <code>p = dot(corners[i], axis)</code> and updating <code>mn</code> and <code>mx</code> with <code>std::min</code> and <code>std::max</code>. Return <code>{ mn, mx }</code>.</li>
</ol>
<p><strong>Success condition:</strong> the project compiles. Neither helper is called from outside this file yet, that starts in the next task.</p>


<div class="code-file-header">SAT.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">#include &quot;SAT.h&quot;
#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;limits&gt;

namespace {
    struct Projection {
        float min;
        float max;
    };

    float dot(sf::Vector2f a, sf::Vector2f b) {
        // TODO: return a.x * b.x + a.y * b.y
        return 0.0f;
    }

    Projection project(const std::array&lt;sf::Vector2f, 4&gt;&amp; corners, sf::Vector2f axis) {
        // TODO: start mn and mx both at dot(corners[0], axis), then loop i
        // from 1 to 3 updating mn with std::min and mx with std::max against
        // dot(corners[i], axis), then return { mn, mx }
        return { 0.0f, 0.0f };
    }
}

bool SAT::aabbOverlap(const OBB&amp; a, const OBB&amp; b) {
    return a.getAABB().findIntersection(b.getAABB()).has_value();
}</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 9 - Testing All Four Axes</h3>
      
<p>Open <code>SAT.cpp</code> and add <code>SAT::test</code> below <code>aabbOverlap</code>. This is the precise narrow-phase test itself: check the two boxes' projections against each of the four candidate axes, exiting the moment one axis proves they cannot be touching.</p>
<ol>
  <li>Get both boxes' corners with <code>a.getCorners()</code> and <code>b.getCorners()</code>, and combine both boxes' axes from <code>a.getAxes()</code> and <code>b.getAxes()</code> into one <code>std::array&lt;sf::Vector2f, 4&gt; axes</code>.</li>
  <li>Declare <code>float bestOverlap = std::numeric_limits&lt;float&gt;::max()</code> and <code>sf::Vector2f bestAxis(0.f, 0.f)</code> to track the smallest overlap seen so far.</li>
  <li>For each <code>axis</code> in <code>axes</code>, project both boxes' corners onto it, then compute <code>overlap = std::min(projA.max, projB.max) - std::max(projA.min, projB.min)</code>.</li>
  <li>If <code>overlap &lt;= 0.f</code>, this axis alone proves separation, return <code>Result{ false, sf::Vector2f(0.f, 0.f), 0.f }</code> immediately without checking the remaining axes.</li>
  <li>Otherwise, if <code>overlap &lt; bestOverlap</code>, update <code>bestOverlap</code> and <code>bestAxis</code>.</li>
</ol>
<p><strong>Success condition:</strong> the project compiles with a working <code>test</code>, but it is not called from <code>main</code> yet, so nothing changes on screen. That is the next task.</p>


<div class="code-file-header">SAT.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">SAT::Result SAT::test(const OBB&amp; a, const OBB&amp; b) {
    std::array&lt;sf::Vector2f, 4&gt; cornersA = a.getCorners();
    std::array&lt;sf::Vector2f, 4&gt; cornersB = b.getCorners();

    // TODO: combine axesA and axesB (from a.getAxes() and b.getAxes()) into
    // one std::array&lt;sf::Vector2f, 4&gt; axes

    float bestOverlap = std::numeric_limits&lt;float&gt;::max();
    sf::Vector2f bestAxis(0.f, 0.f);

    // TODO: for each axis in axes, project cornersA and cornersB onto it,
    // compute overlap = std::min(projA.max, projB.max) - std::max(projA.min, projB.min).
    // If overlap &lt;= 0.f, return Result{ false, sf::Vector2f(0.f, 0.f), 0.f }
    // immediately. Otherwise, if overlap &lt; bestOverlap, update bestOverlap
    // and bestAxis.

    return Result{ false, sf::Vector2f(0.f, 0.f), 0.f };
}</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 10 - Orienting the Push Direction</h3>
      
<p>Open <code>SAT.cpp</code> and finish <code>SAT::test</code>. Reaching the end of the axis loop without an early return means every axis showed overlap, so the boxes are colliding. <code>bestAxis</code> already holds the smallest overlap direction, but it could point either way, this step makes sure the returned normal always points from <code>a</code> toward <code>b</code>.</p>
<ol>
  <li>Below the axis loop, compute <code>sf::Vector2f direction = b.center - a.center</code>.</li>
  <li>If <code>dot(direction, bestAxis) &lt; 0.f</code>, negate <code>bestAxis</code> so it points toward <code>b</code>.</li>
  <li>Return <code>Result{ true, bestAxis, bestOverlap }</code>.</li>
</ol>
<p><strong>Success condition:</strong> the project compiles with a complete <code>SAT::test</code>. It still is not called from <code>main</code> yet, that is the next task.</p>


<div class="code-file-header">SAT.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">// At the end of SAT::test, after the axis loop finds no separating axis:

// TODO: compute sf::Vector2f direction = b.center - a.center. If
// dot(direction, bestAxis) &lt; 0.f, negate bestAxis so the normal points from
// a's centre toward b's. Then return Result{ true, bestAxis, bestOverlap }</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 11 - Confirming Collisions on Screen</h3>
      
<p>Open <code>NarrowPhaseDemo.cpp</code> and replace the flat yellow-line drawing from Task 7 with a real narrow-phase pass, so a candidate pair only stays yellow if SAT rejects it, and turns red once SAT confirms it.</p>
<ol>
  <li>Right after building <code>candidatePairs</code>, declare <code>std::vector&lt;SAT::Result&gt; results(candidatePairs.size())</code>, then loop over every candidate pair calling <code>SAT::test(boxes[pair.first], boxes[pair.second])</code> and storing the result.</li>
  <li>Count how many results have <code>colliding == true</code> into a <code>confirmedCount</code>.</li>
  <li>Replace the single-colour line loop from Task 7: for each candidate pair, if its result is colliding, draw a thick line (use a rectangle-shaped line, 4 pixels wide, in <code>sf::Color(255, 60, 60)</code>) between the two centres. Otherwise, draw the thin yellow line exactly as before.</li>
  <li>Update the stats text to also report <code>confirmedCount</code> and <code>candidatePairs.size() - confirmedCount</code> as the false-positive count.</li>
</ol>
<p><strong>Success condition:</strong> drive the green box into a spinning neighbour. The connecting line turns red only once their true rotated outlines actually touch, staying yellow the whole time only their bounding boxes overlap.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">// Right after building candidatePairs:
// TODO: declare std::vector&lt;SAT::Result&gt; results(candidatePairs.size()),
// then fill it by calling SAT::test(boxes[pair.first], boxes[pair.second])
// for each entry in candidatePairs

// TODO: count how many results have colliding == true into confirmedCount

// Replacing the Task 7 line-drawing loop:
for (const auto&amp; pair : candidatePairs) {
    sf::Vector2f centerA = boxes[pair.first].center;
    sf::Vector2f centerB = boxes[pair.second].center;

    // TODO: if this pair's SAT result is colliding, draw a thick
    // (4 pixel) sf::Color(255, 60, 60) line between centerA and centerB.
    // Otherwise, draw the thin sf::Color(255, 220, 60, 140) line from
    // Task 7 using sf::Vertex and sf::PrimitiveType::Lines.
}

// TODO: extend the stats text to also report confirmedCount and
// (candidatePairs.size() - confirmedCount) as the false-positive count</code></pre>
    </div>

    <div class="task-block">
      <h3>Task 12 - Drawing the Push-Apart Arrow</h3>
      
<p>Open <code>NarrowPhaseDemo.cpp</code>. Add a small arrow-drawing helper and call it for every confirmed collision, so the direction and (scaled up for visibility) size of the minimum push needed to separate a pair is visible directly, not just implied by the red line.</p>
<ol>
  <li>In the anonymous namespace, add the <code>drawThickLine</code> and <code>drawArrow</code> helpers shown below. <code>drawArrow</code> draws a shaft from a start point along a direction for a given length, plus a small two-stroke arrowhead at the tip.</li>
  <li>Add <code>bool showMTV = true;</code> before the main loop, and toggle it on <code>sf::Keyboard::Scancode::M</code> alongside the existing <code>G</code> handler.</li>
  <li>Where a confirmed pair's thick red line is drawn, if <code>showMTV</code> is true, call <code>drawArrow</code> starting at the midpoint between the two centres, using the result's <code>normal</code> as the direction and <code>result.penetration * 4.0f</code> as the length (the multiplier exaggerates small penetrations so they stay visible), coloured <code>sf::Color(255, 160, 30)</code>.</li>
</ol>
<p><strong>Success condition:</strong> pressing <code>M</code> toggles an orange arrow on every confirmed collision, pointing away from the overlap in the direction <code>SAT::test</code> reported as the cheapest separating push.</p>


<div class="code-file-header">NarrowPhaseDemo.cpp</div>
<pre style="border-radius:0 0 6px 6px;margin-top:0"><code class="language-cpp">void drawThickLine(sf::RenderWindow&amp; window, sf::Vector2f from, sf::Vector2f to, float thickness, sf::Color colour) {
    sf::Vector2f delta = to - from;
    float length = std::sqrt(delta.x * delta.x + delta.y * delta.y);
    if (length &lt; 0.0001f) {
        return;
    }

    sf::RectangleShape line(sf::Vector2f(length, thickness));
    line.setOrigin(sf::Vector2f(0.0f, thickness * 0.5f));
    line.setPosition(from);
    float angleDeg = std::atan2(delta.y, delta.x) * 180.0f / 3.14159265f;
    line.setRotation(sf::degrees(angleDeg));
    line.setFillColor(colour);
    window.draw(line);
}

void drawArrow(sf::RenderWindow&amp; window, sf::Vector2f from, sf::Vector2f direction, float length, sf::Color colour) {
    if (length &lt; 0.0001f) {
        return;
    }
    sf::Vector2f tip = from + direction * length;
    drawThickLine(window, from, tip, 3.0f, colour);

    float angle = std::atan2(direction.y, direction.x);
    float headLength = std::min(12.0f, length * 0.4f);
    constexpr float headSpreadRadians = 2.6f;
    sf::Vector2f left(std::cos(angle + headSpreadRadians), std::sin(angle + headSpreadRadians));
    sf::Vector2f right(std::cos(angle - headSpreadRadians), std::sin(angle - headSpreadRadians));
    drawThickLine(window, tip, tip + left * headLength, 3.0f, colour);
    drawThickLine(window, tip, tip + right * headLength, 3.0f, colour);
}

// bool showMTV = true; before the main loop, toggled on sf::Keyboard::Scancode::M

// Where a confirmed pair's thick red line is drawn:
// if (showMTV) {
//     sf::Vector2f midpoint = (centerA + centerB) * 0.5f;
//     drawArrow(window, midpoint, result.normal, result.penetration * 4.0f, sf::Color(255, 160, 30));
// }</code></pre>
    </div>

  </section>

  <section class="lab-section worked-solution">
    <h2>Solution</h2>
    <p>Try the tasks above before revealing the solution.</p>
    <button class="solution-btn" onclick="toggleSolution(this)">Show Solution</button>
    <div class="solution-panel">
      <div class="solution-canvas-wrap"><canvas data-code="#pragma once
#include &lt;SFML/Graphics.hpp&gt;
#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;cmath&gt;

class OBB {
public:
    OBB(sf::Vector2f centerPos, sf::Vector2f halfExtents, float rotationRadians,
        float angularVelocityRadPerSec, sf::Vector2f linearVelocity, sf::Color colour)
        : center(centerPos), halfExtents(halfExtents), rotation(rotationRadians),
          angularVelocity(angularVelocityRadPerSec), velocity(linearVelocity), colour(colour) {}

    void update(float deltaTime, float worldWidth, float worldHeight) {
        rotation += angularVelocity * deltaTime;
        center += velocity * deltaTime;

        float boundRadius = boundingRadius();

        if (center.x - boundRadius &lt; 0.0f) {
            center.x = boundRadius;
            velocity.x = -velocity.x;
        } else if (center.x + boundRadius &gt; worldWidth) {
            center.x = worldWidth - boundRadius;
            velocity.x = -velocity.x;
        }

        if (center.y - boundRadius &lt; 0.0f) {
            center.y = boundRadius;
            velocity.y = -velocity.y;
        } else if (center.y + boundRadius &gt; worldHeight) {
            center.y = worldHeight - boundRadius;
            velocity.y = -velocity.y;
        }
    }

    std::array&lt;sf::Vector2f, 4&gt; getCorners() const {
        float cosR = std::cos(rotation);
        float sinR = std::sin(rotation);

        std::array&lt;sf::Vector2f, 4&gt; localSigns = {
            sf::Vector2f(-halfExtents.x, -halfExtents.y),
            sf::Vector2f(halfExtents.x, -halfExtents.y),
            sf::Vector2f(halfExtents.x, halfExtents.y),
            sf::Vector2f(-halfExtents.x, halfExtents.y)
        };

        std::array&lt;sf::Vector2f, 4&gt; corners;
        for (std::size_t i = 0; i &lt; 4; ++i) {
            float localX = localSigns[i].x;
            float localY = localSigns[i].y;
            corners[i] = sf::Vector2f(
                center.x + localX * cosR - localY * sinR,
                center.y + localX * sinR + localY * cosR);
        }
        return corners;
    }

    std::array&lt;sf::Vector2f, 2&gt; getAxes() const {
        float cosR = std::cos(rotation);
        float sinR = std::sin(rotation);
        return { sf::Vector2f(cosR, sinR), sf::Vector2f(-sinR, cosR) };
    }

    sf::FloatRect getAABB() const {
        auto corners = getCorners();
        float minX = corners[0].x, maxX = corners[0].x;
        float minY = corners[0].y, maxY = corners[0].y;
        for (std::size_t i = 1; i &lt; 4; ++i) {
            minX = std::min(minX, corners[i].x);
            maxX = std::max(maxX, corners[i].x);
            minY = std::min(minY, corners[i].y);
            maxY = std::max(maxY, corners[i].y);
        }
        return sf::FloatRect(sf::Vector2f(minX, minY), sf::Vector2f(maxX - minX, maxY - minY));
    }

    float boundingRadius() const {
        return std::sqrt(halfExtents.x * halfExtents.x + halfExtents.y * halfExtents.y);
    }

    void draw(sf::RenderWindow&amp; window, sf::Color outlineColour, float outlineThickness = 2.0f) const {
        auto corners = getCorners();
        sf::ConvexShape shape(4);
        for (std::size_t i = 0; i &lt; 4; ++i) {
            shape.setPoint(i, corners[i]);
        }
        shape.setFillColor(sf::Color::Transparent);
        shape.setOutlineColor(outlineColour);
        shape.setOutlineThickness(outlineThickness);
        window.draw(shape);
    }

    void drawAABB(sf::RenderWindow&amp; window, sf::Color colour) const {
        sf::FloatRect bounds = getAABB();
        sf::RectangleShape box(bounds.size);
        box.setPosition(bounds.position);
        box.setFillColor(sf::Color::Transparent);
        box.setOutlineColor(colour);
        box.setOutlineThickness(1.0f);
        window.draw(box);
    }

    sf::Vector2f center;
    sf::Vector2f halfExtents;
    float rotation;
    float angularVelocity;
    sf::Vector2f velocity;
    sf::Color colour;
};" data-filename="OBB.h" style="display:block;border-radius:6px"></canvas></div>
      <div class="solution-canvas-wrap"><canvas data-code="#pragma once
#include &lt;SFML/Graphics.hpp&gt;
#include &quot;OBB.h&quot;

namespace SAT {
    struct Result {
        bool colliding = false;
        sf::Vector2f normal{ 0.f, 0.f };
        float penetration = 0.f;
    };

    Result test(const OBB&amp; a, const OBB&amp; b);
    bool aabbOverlap(const OBB&amp; a, const OBB&amp; b);
}" data-filename="SAT.h" style="display:block;border-radius:6px"></canvas></div>
      <div class="solution-canvas-wrap"><canvas data-code="#include &quot;SAT.h&quot;
#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;limits&gt;

namespace {
    struct Projection {
        float min;
        float max;
    };

    float dot(sf::Vector2f a, sf::Vector2f b) {
        return a.x * b.x + a.y * b.y;
    }

    Projection project(const std::array&lt;sf::Vector2f, 4&gt;&amp; corners, sf::Vector2f axis) {
        float mn = dot(corners[0], axis);
        float mx = mn;
        for (std::size_t i = 1; i &lt; 4; ++i) {
            float p = dot(corners[i], axis);
            mn = std::min(mn, p);
            mx = std::max(mx, p);
        }
        return { mn, mx };
    }
}

bool SAT::aabbOverlap(const OBB&amp; a, const OBB&amp; b) {
    return a.getAABB().findIntersection(b.getAABB()).has_value();
}

SAT::Result SAT::test(const OBB&amp; a, const OBB&amp; b) {
    std::array&lt;sf::Vector2f, 4&gt; cornersA = a.getCorners();
    std::array&lt;sf::Vector2f, 4&gt; cornersB = b.getCorners();

    std::array&lt;sf::Vector2f, 2&gt; axesA = a.getAxes();
    std::array&lt;sf::Vector2f, 2&gt; axesB = b.getAxes();
    std::array&lt;sf::Vector2f, 4&gt; axes = { axesA[0], axesA[1], axesB[0], axesB[1] };

    float bestOverlap = std::numeric_limits&lt;float&gt;::max();
    sf::Vector2f bestAxis(0.f, 0.f);

    for (sf::Vector2f axis : axes) {
        Projection projA = project(cornersA, axis);
        Projection projB = project(cornersB, axis);

        float overlap = std::min(projA.max, projB.max) - std::max(projA.min, projB.min);

        if (overlap &lt;= 0.f) {
            return Result{ false, sf::Vector2f(0.f, 0.f), 0.f };
        }

        if (overlap &lt; bestOverlap) {
            bestOverlap = overlap;
            bestAxis = axis;
        }
    }

    sf::Vector2f direction = b.center - a.center;
    if (dot(direction, bestAxis) &lt; 0.f) {
        bestAxis = -bestAxis;
    }

    return Result{ true, bestAxis, bestOverlap };
}" data-filename="SAT.cpp" style="display:block;border-radius:6px"></canvas></div>
      <div class="solution-canvas-wrap"><canvas data-code="#include &lt;SFML/Graphics.hpp&gt;
#include &lt;algorithm&gt;
#include &lt;cmath&gt;
#include &lt;iomanip&gt;
#include &lt;optional&gt;
#include &lt;random&gt;
#include &lt;sstream&gt;
#include &lt;vector&gt;
#include &quot;OBB.h&quot;
#include &quot;SAT.h&quot;

namespace {
    constexpr unsigned int WORLD_WIDTH = 900;
    constexpr unsigned int WORLD_HEIGHT = 800;
    constexpr std::size_t BOX_COUNT = 8;

    constexpr float MOVE_SPEED = 220.0f;
    constexpr float ROTATE_SPEED = 1.6f;

    std::mt19937 rng(std::random_device{}());

    sf::Color randomColour() {
        std::uniform_int_distribution&lt;int&gt; c(100, 255);
        return sf::Color(c(rng), c(rng), c(rng));
    }

    OBB makeRandomBox(float worldWidth, float worldHeight) {
        std::uniform_real_distribution&lt;float&gt; posX(60.0f, worldWidth - 60.0f);
        std::uniform_real_distribution&lt;float&gt; posY(60.0f, worldHeight - 60.0f);
        std::uniform_real_distribution&lt;float&gt; halfExtent(20.0f, 45.0f);
        std::uniform_real_distribution&lt;float&gt; rotationDist(0.0f, 6.2831853f);
        std::uniform_real_distribution&lt;float&gt; angularSpeed(0.25f, 1.6f);
        std::uniform_real_distribution&lt;float&gt; linearSpeed(-40.0f, 40.0f);
        std::bernoulli_distribution flipSign(0.5);
        std::bernoulli_distribution drifts(0.5);

        float angularVelocity = angularSpeed(rng) * (flipSign(rng) ? 1.0f : -1.0f);
        sf::Vector2f velocity = drifts(rng)
            ? sf::Vector2f(linearSpeed(rng), linearSpeed(rng))
            : sf::Vector2f(0.0f, 0.0f);

        return OBB(
            sf::Vector2f(posX(rng), posY(rng)),
            sf::Vector2f(halfExtent(rng), halfExtent(rng)),
            rotationDist(rng),
            angularVelocity,
            velocity,
            randomColour());
    }

    void drawThickLine(sf::RenderWindow&amp; window, sf::Vector2f from, sf::Vector2f to, float thickness, sf::Color colour) {
        sf::Vector2f delta = to - from;
        float length = std::sqrt(delta.x * delta.x + delta.y * delta.y);
        if (length &lt; 0.0001f) {
            return;
        }

        sf::RectangleShape line(sf::Vector2f(length, thickness));
        line.setOrigin(sf::Vector2f(0.0f, thickness * 0.5f));
        line.setPosition(from);
        float angleDeg = std::atan2(delta.y, delta.x) * 180.0f / 3.14159265f;
        line.setRotation(sf::degrees(angleDeg));
        line.setFillColor(colour);
        window.draw(line);
    }

    void drawArrow(sf::RenderWindow&amp; window, sf::Vector2f from, sf::Vector2f direction, float length, sf::Color colour) {
        if (length &lt; 0.0001f) {
            return;
        }
        sf::Vector2f tip = from + direction * length;
        drawThickLine(window, from, tip, 3.0f, colour);

        float angle = std::atan2(direction.y, direction.x);
        float headLength = std::min(12.0f, length * 0.4f);
        constexpr float headSpreadRadians = 2.6f;
        sf::Vector2f left(std::cos(angle + headSpreadRadians), std::sin(angle + headSpreadRadians));
        sf::Vector2f right(std::cos(angle - headSpreadRadians), std::sin(angle - headSpreadRadians));
        drawThickLine(window, tip, tip + left * headLength, 3.0f, colour);
        drawThickLine(window, tip, tip + right * headLength, 3.0f, colour);
    }
}

int main() {
    sf::VideoMode videoMode(sf::Vector2u(WORLD_WIDTH, WORLD_HEIGHT));
    sf::RenderWindow window(videoMode, &quot;Narrow Phase - SAT on Rotating Boxes&quot;);
    window.setFramerateLimit(60);

    sf::Font font;
    font.openFromFile(&quot;C:/Windows/Fonts/arial.ttf&quot;);

    std::vector&lt;OBB&gt; boxes;
    boxes.push_back(OBB(
        sf::Vector2f(WORLD_WIDTH * 0.5f, WORLD_HEIGHT * 0.5f),
        sf::Vector2f(35.0f, 35.0f),
        0.0f, 0.0f,
        sf::Vector2f(0.0f, 0.0f),
        sf::Color(80, 220, 140)));
    for (std::size_t i = 1; i &lt; BOX_COUNT; ++i) {
        boxes.push_back(makeRandomBox(static_cast&lt;float&gt;(WORLD_WIDTH), static_cast&lt;float&gt;(WORLD_HEIGHT)));
    }

    bool showAABB = true;
    bool showMTV = true;
    sf::Clock clock;

    while (window.isOpen()) {
        float deltaTime = clock.restart().asSeconds();

        while (const std::optional event = window.pollEvent()) {
            if (event-&gt;is&lt;sf::Event::Closed&gt;()) {
                window.close();
            }

            if (const auto* keyPressed = event-&gt;getIf&lt;sf::Event::KeyPressed&gt;()) {
                switch (keyPressed-&gt;scancode) {
                    case sf::Keyboard::Scancode::G:
                        showAABB = !showAABB;
                        break;
                    case sf::Keyboard::Scancode::M:
                        showMTV = !showMTV;
                        break;
                    default:
                        break;
                }
            }
        }

        OBB&amp; userBox = boxes[0];
        sf::Vector2f move(0.0f, 0.0f);
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Left) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::A)) {
            move.x -= 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Right) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::D)) {
            move.x += 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Up) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::W)) {
            move.y -= 1.0f;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Down) || sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::S)) {
            move.y += 1.0f;
        }
        userBox.center += move * MOVE_SPEED * deltaTime;
        userBox.center.x = std::clamp(userBox.center.x, 0.0f, static_cast&lt;float&gt;(WORLD_WIDTH));
        userBox.center.y = std::clamp(userBox.center.y, 0.0f, static_cast&lt;float&gt;(WORLD_HEIGHT));

        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::Q)) {
            userBox.rotation -= ROTATE_SPEED * deltaTime;
        }
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Scancode::E)) {
            userBox.rotation += ROTATE_SPEED * deltaTime;
        }

        for (std::size_t i = 1; i &lt; boxes.size(); ++i) {
            boxes[i].update(deltaTime, static_cast&lt;float&gt;(WORLD_WIDTH), static_cast&lt;float&gt;(WORLD_HEIGHT));
        }

        std::vector&lt;std::pair&lt;std::size_t, std::size_t&gt;&gt; candidatePairs;
        for (std::size_t i = 0; i &lt; boxes.size(); ++i) {
            for (std::size_t j = i + 1; j &lt; boxes.size(); ++j) {
                if (SAT::aabbOverlap(boxes[i], boxes[j])) {
                    candidatePairs.emplace_back(i, j);
                }
            }
        }

        std::vector&lt;SAT::Result&gt; results(candidatePairs.size());
        for (std::size_t i = 0; i &lt; candidatePairs.size(); ++i) {
            results[i] = SAT::test(boxes[candidatePairs[i].first], boxes[candidatePairs[i].second]);
        }

        std::size_t confirmedCount = 0;
        for (const auto&amp; result : results) {
            if (result.colliding) {
                ++confirmedCount;
            }
        }

        window.clear(sf::Color(12, 12, 18));

        if (showAABB) {
            for (const auto&amp; box : boxes) {
                box.drawAABB(window, sf::Color(90, 90, 110, 130));
            }
        }

        for (std::size_t i = 0; i &lt; candidatePairs.size(); ++i) {
            sf::Vector2f centerA = boxes[candidatePairs[i].first].center;
            sf::Vector2f centerB = boxes[candidatePairs[i].second].center;

            if (results[i].colliding) {
                drawThickLine(window, centerA, centerB, 4.0f, sf::Color(255, 60, 60));
                if (showMTV) {
                    sf::Vector2f midpoint = (centerA + centerB) * 0.5f;
                    drawArrow(window, midpoint, results[i].normal, results[i].penetration * 4.0f, sf::Color(255, 160, 30));
                }
            } else {
                sf::Vertex line[] = {
                    sf::Vertex(centerA, sf::Color(255, 220, 60, 140)),
                    sf::Vertex(centerB, sf::Color(255, 220, 60, 140))
                };
                window.draw(line, 2, sf::PrimitiveType::Lines);
            }
        }

        for (const auto&amp; box : boxes) {
            box.draw(window, box.colour, 2.0f);
        }

        std::ostringstream stats;
        stats &lt;&lt; std::fixed &lt;&lt; std::setprecision(2);
        stats &lt;&lt; &quot;boxes: &quot; &lt;&lt; boxes.size() &lt;&lt; &quot;\n&quot;
              &lt;&lt; &quot;AABB candidate pairs: &quot; &lt;&lt; candidatePairs.size() &lt;&lt; &quot;\n&quot;
              &lt;&lt; &quot;SAT confirmed collisions: &quot; &lt;&lt; confirmedCount &lt;&lt; &quot;\n&quot;
              &lt;&lt; &quot;broad-phase false positives: &quot; &lt;&lt; (candidatePairs.size() - confirmedCount);

        sf::Text statsText(font, stats.str(), 20);
        statsText.setFillColor(sf::Color::White);
        statsText.setPosition(sf::Vector2f(20.0f, 20.0f));
        window.draw(statsText);

        window.display();
    }

    return 0;
}" data-filename="NarrowPhaseDemo.cpp" style="display:block;border-radius:6px"></canvas></div>
    </div>
  </section>
</main>

<script>
  function switchTab(e, id) {
    const section = e.target.closest('.lab-section');
    section.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
    section.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
    e.target.classList.add('active');
    section.querySelector('#' + id).classList.add('active');
  }

  function renderSolutionCanvas(canvas, code, filename) {
    var ctx = canvas.getContext('2d');
    var dpr = window.devicePixelRatio || 1;
    var lineHeight = 19;
    var headerHeight = filename ? 28 : 0;
    var padX = 20;
    var padY = 14;
    var codeFont = '13px Consolas, "Courier New", monospace';
    ctx.font = codeFont;
    var lines = code.split('\n').map(function(l) { return l.replace(/\r$/, ''); });
    while (lines.length && lines[lines.length - 1] === '') lines.pop();
    var maxW = 0;
    for (var i = 0; i < lines.length; i++) {
      var w = ctx.measureText(lines[i]).width;
      if (w > maxW) maxW = w;
    }
    var containerW = canvas.parentElement ? (canvas.parentElement.clientWidth || 0) : 0;
    var cssW = Math.max(maxW + padX * 2, containerW || 400);
    var cssH = lines.length * lineHeight + padY * 2 + headerHeight;
    canvas.width = Math.round(cssW * dpr);
    canvas.height = Math.round(cssH * dpr);
    canvas.style.width = cssW + 'px';
    canvas.style.height = cssH + 'px';
    ctx.scale(dpr, dpr);
    if (filename) {
      ctx.fillStyle = '#2d2d2d';
      ctx.fillRect(0, 0, cssW, headerHeight);
      ctx.font = '11px Consolas, "Courier New", monospace';
      ctx.fillStyle = '#888888';
      ctx.textBaseline = 'middle';
      ctx.fillText(filename, padX, headerHeight / 2);
      ctx.fillStyle = '#444444';
      ctx.fillRect(0, headerHeight - 1, cssW, 1);
    }
    ctx.fillStyle = '#1E1E1E';
    ctx.fillRect(0, headerHeight, cssW, cssH - headerHeight);
    ctx.font = codeFont;
    ctx.fillStyle = '#cccccc';
    ctx.textBaseline = 'top';
    for (var i = 0; i < lines.length; i++) {
      ctx.fillText(lines[i], padX, headerHeight + padY + i * lineHeight);
    }
  }

  function toggleSolution(btn) {
    var panel = btn.nextElementSibling;
    var open = panel.classList.toggle('open');
    btn.textContent = open ? 'Hide Solution' : 'Show Solution';
    if (open) {
      panel.querySelectorAll('canvas[data-code]').forEach(function(c) {
        if (!c.dataset.rendered) {
          renderSolutionCanvas(c, c.dataset.code, c.dataset.filename || '');
          c.dataset.rendered = '1';
        }
      });
    }
  }

  function checkBlanks(btn) {
    var task = btn.closest('.task-block');
    task.querySelectorAll('.blank-question').forEach(function(q) {
      var answers = JSON.parse(q.dataset.answers);
      var inputs = q.querySelectorAll('.blank-input');
      var allCorrect = true;
      inputs.forEach(function(input, i) {
        var expected = [].concat(answers[i]).map(function(s) { return s.toLowerCase().trim(); });
        var val = input.value.toLowerCase().trim();
        var ok = expected.indexOf(val) !== -1;
        input.classList.toggle('blank-correct', ok);
        input.classList.toggle('blank-incorrect', !ok && val !== '');
        if (!ok) allCorrect = false;
      });
      var fb = q.querySelector('.blank-feedback');
      fb.textContent = allCorrect ? 'Correct!' : 'Check your answer.';
      fb.className = 'blank-feedback ' + (allCorrect ? 'feedback-correct' : 'feedback-incorrect');
    });
  }

  function revealMCAnswer(radio) {
    var item    = radio.closest('.mc-item');
    var correct = parseInt(item.dataset.correct, 10);
    var radios  = Array.from(item.querySelectorAll('input[type=radio]'));
    var chosen  = radios.indexOf(radio);
    item.querySelectorAll('.mc-option').forEach(function(opt, i) {
      opt.classList.toggle('mc-correct',   i === correct);
      opt.classList.toggle('mc-incorrect', i === chosen && i !== correct);
    });
    var fb = item.querySelector('.mc-feedback');
    if (fb) {
      fb.textContent = chosen === correct ? 'Correct!' : 'Incorrect.';
      fb.className   = 'mc-feedback ' + (chosen === correct ? 'feedback-correct' : 'feedback-incorrect');
    }
    var exp = item.querySelector('.mc-explanation');
    if (exp) exp.classList.add('visible');
    item.querySelectorAll('input[type=radio]').forEach(function(r) { r.disabled = true; });
  }

  function toggleModelAnswer(btn) {
    var panel = btn.nextElementSibling;
    var open  = panel.classList.toggle('visible');
    btn.textContent = open ? 'Hide Model Answer' : 'Show Model Answer';
  }
</script>

<script>/**
 * Minified by jsDelivr using Terser v5.39.0.
 * Original file: /npm/prismjs@1.29.0/prism.js
 *
 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
 */
var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,a={},r={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof s?new s(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++n}),e.__id},clone:function e(t,n){var a,s;switch(n=n||{},r.util.type(t)){case"Object":if(s=r.util.objId(t),n[s])return n[s];for(var i in a={},n[s]=a,t)t.hasOwnProperty(i)&&(a[i]=e(t[i],n));return a;case"Array":return s=r.util.objId(t),n[s]?n[s]:(a=[],n[s]=a,t.forEach((function(t,r){a[r]=e(t,n)})),a);default:return t}},getLanguage:function(e){for(;e;){var n=t.exec(e.className);if(n)return n[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,n){e.className=e.className.replace(RegExp(t,"gi"),""),e.classList.add("language-"+n)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(a){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(a.stack)||[])[1];if(e){var t=document.getElementsByTagName("script");for(var n in t)if(t[n].src==e)return t[n]}return null}},isActive:function(e,t,n){for(var a="no-"+t;e;){var r=e.classList;if(r.contains(t))return!0;if(r.contains(a))return!1;e=e.parentElement}return!!n}},languages:{plain:a,plaintext:a,text:a,txt:a,extend:function(e,t){var n=r.util.clone(r.languages[e]);for(var a in t)n[a]=t[a];return n},insertBefore:function(e,t,n,a){var s=(a=a||r.languages)[e],i={};for(var o in s)if(s.hasOwnProperty(o)){if(o==t)for(var l in n)n.hasOwnProperty(l)&&(i[l]=n[l]);n.hasOwnProperty(o)||(i[o]=s[o])}var u=a[e];return a[e]=i,r.languages.DFS(r.languages,(function(t,n){n===u&&t!=e&&(this[t]=i)})),i},DFS:function e(t,n,a,s){s=s||{};var i=r.util.objId;for(var o in t)if(t.hasOwnProperty(o)){n.call(t,o,t[o],a||o);var l=t[o],u=r.util.type(l);"Object"!==u||s[i(l)]?"Array"!==u||s[i(l)]||(s[i(l)]=!0,e(l,n,o,s)):(s[i(l)]=!0,e(l,n,null,s))}}},plugins:{},highlightAll:function(e,t){r.highlightAllUnder(document,e,t)},highlightAllUnder:function(e,t,n){var a={callback:n,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};r.hooks.run("before-highlightall",a),a.elements=Array.prototype.slice.apply(a.container.querySelectorAll(a.selector)),r.hooks.run("before-all-elements-highlight",a);for(var s,i=0;s=a.elements[i++];)r.highlightElement(s,!0===t,a.callback)},highlightElement:function(t,n,a){var s=r.util.getLanguage(t),i=r.languages[s];r.util.setLanguage(t,s);var o=t.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&r.util.setLanguage(o,s);var l={element:t,language:s,grammar:i,code:t.textContent};function u(e){l.highlightedCode=e,r.hooks.run("before-insert",l),l.element.innerHTML=l.highlightedCode,r.hooks.run("after-highlight",l),r.hooks.run("complete",l),a&&a.call(l.element)}if(r.hooks.run("before-sanity-check",l),(o=l.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!l.code)return r.hooks.run("complete",l),void(a&&a.call(l.element));if(r.hooks.run("before-highlight",l),l.grammar)if(n&&e.Worker){var g=new Worker(r.filename);g.onmessage=function(e){u(e.data)},g.postMessage(JSON.stringify({language:l.language,code:l.code,immediateClose:!0}))}else u(r.highlight(l.code,l.grammar,l.language));else u(r.util.encode(l.code))},highlight:function(e,t,n){var a={code:e,grammar:t,language:n};if(r.hooks.run("before-tokenize",a),!a.grammar)throw new Error('The language "'+a.language+'" has no grammar.');return a.tokens=r.tokenize(a.code,a.grammar),r.hooks.run("after-tokenize",a),s.stringify(r.util.encode(a.tokens),a.language)},tokenize:function(e,t){var n=t.rest;if(n){for(var a in n)t[a]=n[a];delete t.rest}var r=new l;return u(r,r.head,e),o(e,r,t,r.head,0),function(e){var t=[],n=e.head.next;for(;n!==e.tail;)t.push(n.value),n=n.next;return t}(r)},hooks:{all:{},add:function(e,t){var n=r.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=r.hooks.all[e];if(n&&n.length)for(var a,s=0;a=n[s++];)a(t)}},Token:s};function s(e,t,n,a){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length}function i(e,t,n,a){e.lastIndex=t;var r=e.exec(n);if(r&&a&&r[1]){var s=r[1].length;r.index+=s,r[0]=r[0].slice(s)}return r}function o(e,t,n,a,l,c){for(var d in n)if(n.hasOwnProperty(d)&&n[d]){var p=n[d];p=Array.isArray(p)?p:[p];for(var m=0;m<p.length;++m){if(c&&c.cause==d+","+m)return;var h=p[m],f=h.inside,v=!!h.lookbehind,b=!!h.greedy,y=h.alias;if(b&&!h.pattern.global){var F=h.pattern.toString().match(/[imsuy]*$/)[0];h.pattern=RegExp(h.pattern.source,F+"g")}for(var x=h.pattern||h,k=a.next,w=l;k!==t.tail&&!(c&&w>=c.reach);w+=k.value.length,k=k.next){var A=k.value;if(t.length>e.length)return;if(!(A instanceof s)){var P,$=1;if(b){if(!(P=i(x,w,e,v))||P.index>=e.length)break;var S=P.index,E=P.index+P[0].length,_=w;for(_+=k.value.length;S>=_;)_+=(k=k.next).value.length;if(w=_-=k.value.length,k.value instanceof s)continue;for(var j=k;j!==t.tail&&(_<E||"string"==typeof j.value);j=j.next)$++,_+=j.value.length;$--,A=e.slice(w,_),P.index-=w}else if(!(P=i(x,0,A,v)))continue;S=P.index;var C=P[0],L=A.slice(0,S),z=A.slice(S+C.length),O=w+A.length;c&&O>c.reach&&(c.reach=O);var T=k.prev;if(L&&(T=u(t,T,L),w+=L.length),g(t,T,$),k=u(t,T,new s(d,f?r.tokenize(C,f):C,y,C)),z&&u(t,k,z),$>1){var M={cause:d+","+m,reach:O};o(e,t,n,k.prev,w,M),c&&M.reach>c.reach&&(c.reach=M.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function u(e,t,n){var a=t.next,r={value:n,prev:t,next:a};return t.next=r,a.prev=r,e.length++,r}function g(e,t,n){for(var a=t.next,r=0;r<n&&a!==e.tail;r++)a=a.next;t.next=a,a.prev=t,e.length-=r}if(e.Prism=r,s.stringify=function e(t,n){if("string"==typeof t)return t;if(Array.isArray(t)){var a="";return t.forEach((function(t){a+=e(t,n)})),a}var s={type:t.type,content:e(t.content,n),tag:"span",classes:["token",t.type],attributes:{},language:n},i=t.alias;i&&(Array.isArray(i)?Array.prototype.push.apply(s.classes,i):s.classes.push(i)),r.hooks.run("wrap",s);var o="";for(var l in s.attributes)o+=" "+l+'="'+(s.attributes[l]||"").replace(/"/g,"&quot;")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'"'+o+">"+s.content+"</"+s.tag+">"},!e.document)return e.addEventListener?(r.disableWorkerMessageHandler||e.addEventListener("message",(function(t){var n=JSON.parse(t.data),a=n.language,s=n.code,i=n.immediateClose;e.postMessage(r.highlight(s,r.languages[a],a)),i&&e.close()}),!1),r):r;var c=r.util.currentScript();function d(){r.manual||r.highlightAll()}if(c&&(r.filename=c.src,c.hasAttribute("data-manual")&&(r.manual=!0)),!r.manual){var p=document.readyState;"loading"===p||"interactive"===p&&c&&c.defer?document.addEventListener("DOMContentLoaded",d):window.requestAnimationFrame?window.requestAnimationFrame(d):window.setTimeout(d,16)}return r}(_self);
/**
 * Prism: Lightweight, robust, elegant syntax highlighting
 *
 * @license MIT <https://opensource.org/licenses/MIT>
 * @author Lea Verou <https://lea.verou.me>
 * @namespace
 * @public
 */"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^<!\[CDATA\[|\]\]>$/i;var a={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:n}};a["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]};var r={};r[e]={pattern:RegExp(/(<__[^>]*>)(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?(?=<\/__>)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:a},Prism.languages.insertBefore("markup","cdata",r)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"))}(Prism),Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),Prism.languages.js=Prism.languages.javascript,function(){if(void 0!==Prism&&"undefined"!=typeof document){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector);var e={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},t="data-src-status",n="loading",a="loaded",r="pre[data-src]:not(["+t+'="'+a+'"]):not(['+t+'="'+n+'"])';Prism.hooks.add("before-highlightall",(function(e){e.selector+=", "+r})),Prism.hooks.add("before-sanity-check",(function(s){var i=s.element;if(i.matches(r)){s.code="",i.setAttribute(t,n);var o=i.appendChild(document.createElement("CODE"));o.textContent="Loading…";var l=i.getAttribute("data-src"),u=s.language;if("none"===u){var g=(/\.(\w+)$/.exec(l)||[,"none"])[1];u=e[g]||g}Prism.util.setLanguage(o,u),Prism.util.setLanguage(i,u);var c=Prism.plugins.autoloader;c&&c.loadLanguages(u),function(e,t,n){var a=new XMLHttpRequest;a.open("GET",e,!0),a.onreadystatechange=function(){4==a.readyState&&(a.status<400&&a.responseText?t(a.responseText):a.status>=400?n("✖ Error "+a.status+" while fetching file: "+a.statusText):n("✖ Error: File does not exist or is empty"))},a.send(null)}(l,(function(e){i.setAttribute(t,a);var n=function(e){var t=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||"");if(t){var n=Number(t[1]),a=t[2],r=t[3];return a?r?[n,Number(r)]:[n,void 0]:[n,n]}}(i.getAttribute("data-range"));if(n){var r=e.split(/\r\n?|\n/g),s=n[0],l=null==n[1]?r.length:n[1];s<0&&(s+=r.length),s=Math.max(0,Math.min(s-1,r.length)),l<0&&(l+=r.length),l=Math.max(0,Math.min(l,r.length)),e=r.slice(s,l).join("\n"),i.hasAttribute("data-start")||i.setAttribute("data-start",String(s+1))}o.textContent=e,Prism.highlightElement(o)}),(function(e){i.setAttribute(t,"failed"),o.textContent=e}))}})),Prism.plugins.fileHighlight={highlight:function(e){for(var t,n=(e||document).querySelectorAll(r),a=0;t=n[a++];)Prism.highlightElement(t)}};var s=!1;Prism.fileHighlight=function(){s||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),s=!0),Prism.plugins.fileHighlight.highlight.apply(this,arguments)}}}();
//# sourceMappingURL=/sm/1f55244a569fc0911044b5e4725a97872b6d85b0662a3429a809bb62f713c381.map
Prism.languages.c=Prism.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),Prism.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],char:Prism.languages.c.char,comment:Prism.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}}}),Prism.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete Prism.languages.c.boolean;
!function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n="\\b(?!<keyword>)\\w+(?:\\s*\\.\\s*\\w+)*\\b".replace(/<keyword>/g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp("(\\b(?:class|concept|enum|struct|typename)\\s+)(?!<keyword>)\\w+".replace(/<keyword>/g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp('(\\b(?:import|module)\\s+)(?:"(?:\\\\(?:\r\n|[^])|[^"\\\\\r\n])*"|<[^<>\r\n]*>|'+"<mod-name>(?:\\s*:\\s*<mod-name>)?|:\\s*<mod-name>".replace(/<mod-name>/g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(Prism);</script>

</body>
</html>