i have a line that is draggable by the user.
the user can click and drag anywhere on the line which will move the linepoint closest to the cursor.

i wish to modify this code to generate an interactable interpolated fractal pattern.

I shall explain what I mean.

If i have a line of rank 1, there will be 3 lines on rank 2 tied to the line following these conditions:
The line on the left's first point is attached 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 line on the right's first point is attached 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.



<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Three.js Draggable Line</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);
}
window.addEventListener('resize', onWindowResize);

// Start 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 line code
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(-50, 0, 0),
  new THREE.Vector3(50, 0, 0)
]);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.frustumCulled = false;
scene.add(line);

// Set the distance for grabbing the line (in world units)
let 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) {
    isPanning = true;
    previousMousePosition.set(event.clientX, event.clientY);
  } else if (event.button === 0) {
    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(line, true);

    if (intersects.length > 0) {
      const positions = lineGeometry.attributes.position.array;
      const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
      const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);

      mouseWorld.copy(intersects[0].point);

      const distanceToStart = mouseWorld.distanceTo(lineStart);
      const distanceToEnd = mouseWorld.distanceTo(lineEnd);

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

      isDraggingEndpoint = true;
      previousMousePosition.set(mouse.x, mouse.y);
    }
  }
}

// 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
    );

    previousMouseWorld.set(previousMousePosition.x, previousMousePosition.y, 0).unproject(camera);
    mouseWorld.set(currentMouse.x, currentMouse.y, 0).unproject(camera);

    deltaWorld.subVectors(mouseWorld, previousMouseWorld);

    const positions = lineGeometry.attributes.position.array;
    const index = selectedEndpointIndex * 3;

    positions[index] += deltaWorld.x;
    positions[index + 1] += deltaWorld.y;

    lineGeometry.attributes.position.needsUpdate = true;
    lineGeometry.computeBoundingSphere();

    previousMousePosition.copy(currentMouse);
  }
}

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

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

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