make the middle lines have a zig-zag fractal pattern underneath them consisting of three lines

First point should be the start point of the middle line
Second point should be: 
x: 1/3 of way down middle line (to the middle)
y: 1/3 of either left of right line of the same rank
Third point should be:
x: 2/3 of way down middle line (to the end of the middle line)
y: 1/3 of either left of right line of the same rank DOWN * 2
Fourth point should be the end point of the middle line


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>High-Performance Three.js Fractal Pattern</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="module">
    import * as THREE from './three.module.js';

    // Scene setup
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);

    // Camera setup
    const aspectRatio = window.innerWidth / window.innerHeight;
    const cameraSize = 200;
    const camera = new THREE.OrthographicCamera(
      -cameraSize * aspectRatio,
      cameraSize * aspectRatio,
      cameraSize,
      -cameraSize,
      1,
      1000
    );
    camera.position.set(0, 0, 500);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Zoom logic (based on mouse wheel and key modifiers)
    const zoomSpeed = 0.8;

    function onWheel(event) {
      event.preventDefault();

      const rect = renderer.domElement.getBoundingClientRect();
      const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
      const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;

      const mouseWorld = new THREE.Vector3(mouseX, mouseY, 0).unproject(camera);
      const zoomFactor = 1 - event.deltaY * 0.001 * zoomSpeed;

      const ctrlPressed = event.ctrlKey;
      const shiftPressed = event.shiftKey;

      if (ctrlPressed) {
        camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
        camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
      } else if (shiftPressed) {
        camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
        camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
      } else {
        camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
        camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
        camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
        camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
      }

      camera.updateProjectionMatrix();
    }

    // Prevent default browser behavior for wheel scrolling with Control or Shift key
    window.addEventListener('wheel', onWheel, { passive: false });

    // Prevent default context menu on right-click
    window.addEventListener('contextmenu', (event) => {
      event.preventDefault();
    });

    // Render loop
    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }

    // Handle window resize
    function onWindowResize() {
      const aspect = window.innerWidth / window.innerHeight;
      camera.left = -cameraSize * aspect;
      camera.right = cameraSize * aspect;
      camera.top = cameraSize;
      camera.bottom = -cameraSize;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
      regenerateFractal();
    }
    window.addEventListener('resize', onWindowResize);

    // Initialize animation
    animate();

    // Panning functionality
    let isPanning = false;
    let isDraggingEndpoint = false;
    let selectedEndpointIndex = null;
    const previousMousePosition = new THREE.Vector2();

    // Raycaster for detecting line interaction
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    // Interactive main line code
    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
    const mainLineGeometry = new THREE.BufferGeometry();
    const mainLinePositions = new Float32Array([
      -50, 0, 0,  // Start point
      50, 0, 0    // End point
    ]);
    mainLineGeometry.setAttribute('position', new THREE.BufferAttribute(mainLinePositions, 3));
    const mainLine = new THREE.Line(mainLineGeometry, lineMaterial);
    mainLine.frustumCulled = false;
    scene.add(mainLine);

    // Fractal lines as a single LineSegments object
    const fractalMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
    const fractalGeometry = new THREE.BufferGeometry();
    const maxRanks = 15; // Parameter to control number of ranks

    // Calculate total number of lines based on maxRanks
    const totalLines = (Math.pow(3, maxRanks + 1) - 3) / 2;
    const fractalPositions = new Float32Array(totalLines * 2 * 3); // Each line has 2 points, each point has 3 coordinates
    fractalGeometry.setAttribute('position', new THREE.BufferAttribute(fractalPositions, 3));
    fractalGeometry.setDrawRange(0, 0); // Initially no fractal lines
    fractalGeometry.attributes.position.setUsage(THREE.DynamicDrawUsage); // Inform Three.js that we'll be updating this buffer frequently
    const fractalLineSegments = new THREE.LineSegments(fractalGeometry, fractalMaterial);
    fractalLineSegments.frustumCulled = false;
    scene.add(fractalLineSegments);

    /**
     * Function to regenerate the fractal lines based on the current main line and maxRanks.
     * This function uses an iterative, rank-by-rank approach to generate all lines up to maxRanks.
     */
    function regenerateFractal() {
      const positions = fractalGeometry.attributes.position.array;
      let offset = 0;

      const startX = mainLinePositions[0];
      const startY = mainLinePositions[1];
      const startZ = mainLinePositions[2];
      const endX = mainLinePositions[3];
      const endY = mainLinePositions[4];
      const endZ = mainLinePositions[5];

      // Initialize array for the current lines (starting with the main line)
      let currentLines = [{
        x1: startX,
        y1: startY,
        z1: startZ,
        x2: endX,
        y2: endY,
        z2: endZ
      }];

      // Iterate through each rank
      for (let rank = 1; rank <= maxRanks; rank++) {
        const nextLines = [];

        // Process each line in the current rank
        for (const line of currentLines) {
          const deltaX = line.x2 - line.x1;
          const deltaY = line.y2 - line.y1;
          const z = line.z1; // Assuming z is constant

          // Calculate intermediate points based on the given conditions
          const x0_2 = line.x1 + deltaX / 3;
          const y0_2 = line.y1 + deltaY / 2;
          const x1_2 = line.x2 - deltaX / 3;
          const y1_2 = line.y2 - deltaY / 2;

          // Line 1: from (x1, y1, z) to (x0_2, y0_2, z)
          positions[offset++] = line.x1;
          positions[offset++] = line.y1;
          positions[offset++] = z;
          positions[offset++] = x0_2;
          positions[offset++] = y0_2;
          positions[offset++] = z;
          nextLines.push({
            x1: line.x1,
            y1: line.y1,
            z1: z,
            x2: x0_2,
            y2: y0_2,
            z2: z
          });

          // Line 2: from (x2, y2, z) to (x1_2, y1_2, z)
          positions[offset++] = line.x2;
          positions[offset++] = line.y2;
          positions[offset++] = z;
          positions[offset++] = x1_2;
          positions[offset++] = y1_2;
          positions[offset++] = z;
          nextLines.push({
            x1: x1_2,
            y1: y1_2,
            z1: z,
            x2: line.x2,
            y2: line.y2,
            z2: z
          });

          // Line 3: from (x0_2, y0_2, z) to (x1_2, y1_2, z)
          positions[offset++] = x0_2;
          positions[offset++] = y0_2;
          positions[offset++] = z;
          positions[offset++] = x1_2;
          positions[offset++] = y1_2;
          positions[offset++] = z;
          nextLines.push({
            x1: x0_2,
            y1: y0_2,
            z1: z,
            x2: x1_2,
            y2: y1_2,
            z2: z
          });
        }

        // Prepare for the next rank
        currentLines = nextLines;
      }

      // Update the draw range based on the number of vertices generated
      fractalGeometry.setDrawRange(0, offset / 3);

      // Mark the attribute as needing an update
      fractalGeometry.attributes.position.needsUpdate = true;

      // Optional: Compute the bounding sphere for better rendering performance (can be skipped if unnecessary)
      fractalGeometry.computeBoundingSphere();

      // Debugging: Log the total number of lines generated
      console.log(`Fractal regenerated with maxRanks=${maxRanks}. Total Lines: ${offset / 6}`);
    }

    // Initial fractal generation
    regenerateFractal();

    /**
     * Function to update fractal lines when the main line changes.
     * This function cleans up any existing fractal lines and regenerates them based on the new main line.
     */
    function updateFractalLines() {
      // Since we're reusing the buffer, simply regenerate the fractal
      regenerateFractal();
    }

    // Set the distance for grabbing the line (in world units)
    const distanceForGrab = 2;

    // Set the raycaster's line precision
    raycaster.params.Line.threshold = distanceForGrab;

    // Predefine vectors to avoid excessive allocations
    const mouseWorld = new THREE.Vector3();
    const deltaWorld = new THREE.Vector3();
    const currentMouse = new THREE.Vector2();
    const previousMouseWorld = new THREE.Vector3();

    // Mouse down event
    function onMouseDown(event) {
      if (event.button === 2) { // Right-click for panning
        isPanning = true;
        previousMousePosition.set(event.clientX, event.clientY);
      } else if (event.button === 0) { // Left-click for dragging endpoints
        const rect = renderer.domElement.getBoundingClientRect();
        mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        mouse.y = - ((event.clientY - rect.top) / rect.height) * 2 + 1;

        raycaster.setFromCamera(mouse, camera);

        const intersects = raycaster.intersectObject(mainLine, true);

        if (intersects.length > 0) {
          const intersectPoint = intersects[0].point;
          const startX = mainLinePositions[0];
          const startY = mainLinePositions[1];
          const endX = mainLinePositions[3];
          const endY = mainLinePositions[4];

          const distanceToStart = intersectPoint.distanceTo(new THREE.Vector3(startX, startY, 0));
          const distanceToEnd = intersectPoint.distanceTo(new THREE.Vector3(endX, endY, 0));

          selectedEndpointIndex = (distanceToStart < distanceToEnd) ? 0 : 1;

          isDraggingEndpoint = true;
          previousMousePosition.set(event.clientX, event.clientY);
        }
      }
    }

    // Mouse move event
    function onMouseMove(event) {
      if (isPanning) {
        const deltaX = event.clientX - previousMousePosition.x;
        const deltaY = event.clientY - previousMousePosition.y;

        const zoomFactorX = camera.right - camera.left;
        const zoomFactorY = camera.top - camera.bottom;

        const panX = (deltaX / window.innerWidth) * zoomFactorX;
        const panY = (deltaY / window.innerHeight) * zoomFactorY;

        camera.left -= panX;
        camera.right -= panX;
        camera.top += panY;
        camera.bottom += panY;
        camera.updateProjectionMatrix();

        previousMousePosition.set(event.clientX, event.clientY);
      } else if (isDraggingEndpoint) {
        const rect = renderer.domElement.getBoundingClientRect();
        currentMouse.set(
          ((event.clientX - rect.left) / rect.width) * 2 - 1,
          - ((event.clientY - rect.top) / rect.height) * 2 + 1
        );

        // Convert screen coordinates to world coordinates
        const projector = new THREE.Vector3(currentMouse.x, currentMouse.y, 0).unproject(camera);
        const prevProjector = new THREE.Vector3(
          ((previousMousePosition.x - rect.left) / rect.width) * 2 - 1,
          - ((previousMousePosition.y - rect.top) / rect.height) * 2 + 1,
          0
        ).unproject(camera);

        deltaWorld.subVectors(projector, prevProjector);

        // Update the selected endpoint
        const index = selectedEndpointIndex * 3;
        mainLinePositions[index] += deltaWorld.x;
        mainLinePositions[index + 1] += deltaWorld.y;

        // Mark the position attribute as needing update
        mainLineGeometry.attributes.position.needsUpdate = true;
        mainLineGeometry.computeBoundingSphere();

        previousMousePosition.set(event.clientX, event.clientY);

        // Update fractal lines after changing the main line
        updateFractalLines();
      }
    }

    // Mouse up event
    function onMouseUp(event) {
      if (event.button === 2) { // Right-click
        isPanning = false;
      } else if (event.button === 0) { // Left-click
        isDraggingEndpoint = false;
      }
    }

    // Add event listeners for mouse actions
    window.addEventListener('mousedown', onMouseDown);
    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('mouseup', onMouseUp);

  </script>
</body>
</html>