Lets refer back to my original thesis for the fractal pattern

The main existing line is of rank 1. Under rank 1 line there will be 3 lines on rank 2 following these conditions:
The first line's first point is tied to the leftmost vertex of the rank 1 line. This line's other point is defined as such:
x: 1/3 of way across rank 1 line's x (toward the middle)
y: 1/2 of way across rank 1 line's y (toward the middle)

The second line's first point is tied to the rightmost vertex of the rank 1 line. This line's other point is defined as such:
x: 1/3 of way across rank 1 line's x (toward the middle)
y: 1/2 of way across rank 1 line's y (toward the middle)

The line in the middle joins the leftmost and rightmost lines.


Currently the 'line in the middle' does not have a fractal underneath it.
I'd like the line in the middle to have, in the fractal underneath, a zig-zag pattern based on these dimensions:

The zigzag should consist of 3 lines.

The first line's first point is tied to the leftmost vertex of the middle line. This line's other point is defined as such:
x: 1/3 of way across the middle line's x (toward the middle)
y: 1/3 of the height of either of the two other lines on the same rank (there is left, middle and right lines - either the left or the right's height as they are the same)

The second line's first point is tied to the rightmost vertex of the middle line. This line's other point is defined as such:
x: 1/3 of way across the middle line's x (toward the middle)
y: 1/3 of the height of either of the two other lines on the same rank (there is left, middle and right lines - either the left or the right's height as they are the same)

The last line joins the leftmost and rightmost lines in the middle.

This should create a sort of zig-zag pattern in fractal underneath the main middle line.


This piece is important:

THE ZIG-ZAG PATTERN SHOULD HAVE A FRACTAL UNDERNEATH IT TOO. THIS FRACTAL SHOULD HAVE THE SAME DIMENSIONS AS THE ORIGINAL FRACTAL INCLUDING THE ZIG-ZAG.


<!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 = 10; // 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>