Drawing Line2 Issue

Hello everyone,

First of all, a big thank you to the Three.js support team and the community for all the amazing work and help!

I’m currently working on drawing a polygon using Line2 from three.js (via three/examples/jsm/lines/Line2) to render thicker lines. However, I’m encountering an issue:

  • When I start drawing, the first line segment is visible, but any subsequent lines do not render.
  • The points array is updating correctly, and I am using setPositions() to update LineGeometry, but still, the additional lines don’t appear.

Here’s what I have checked so far:

  • Ensured that setPositions() is updating correctly.
  • Verified that computeLineDistances() is being called.
  • Tried disposing and re-creating LineGeometry after updates.
  • Confirmed that LineMaterial has a resolution set (material.resolution.set(window.innerWidth, window.innerHeight)).

Despite these checks, I’m still facing this issue. Has anyone encountered a similar problem? Any insights would be greatly appreciated!

import React, { useCallback, useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { useFrame, useThree } from '@react-three/fiber';
import useThreeStore from '@store/threeStore';
import { getDistanceFromVector3, getPointFromMouseEvent, projectPointOntoPlane, generatePrismCutShape, movePointsAwayFromPlane } from '@utils/canvas/3d/helpers';
import { Line2 } from 'three/examples/jsm/lines/Line2';
import { CutStep, ThreeWallType } from 'src/typings/types';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry';

interface CutItemProps { }

const CutItem: React.FC<CutItemProps> = () => {
  const { cutStep, cameraControls, setCutStep, setIsDragging, addCutShape, cutShapes } = useThreeStore();
  const { selectedItem } = useThreeStore();
  const { gl, camera, raycaster, scene } = useThree();
  const drawingStartPoint = useRef<THREE.Vector3>(new THREE.Vector3());
  const drawingEndPoint = useRef<THREE.Vector3>(new THREE.Vector3());
  const points = useRef<THREE.Vector3[]>([]);
  const [line] = useState<Line2>(
    new Line2(
      new LineGeometry(),
      new LineMaterial({
        color: 0x00bbff,
        linewidth: 5, // Line width in world units
        dashed: false,
        // worldUnits: true,
        alphaToCoverage: false,
        resolution: new THREE.Vector2(window.innerWidth, window.innerHeight),
      })
    ));
  const wallThickness = useRef<number>(1);
  const substraction = useRef<number>(0.01);

  const updateLineGeometry = useCallback(() => {
    if (!line || points.current.length < 1) {
      return;
    }
    const positions = points.current.flatMap(p => [p.x, p.y, p.z]);
    console.log(points.current.length);
    line.geometry.setDrawRange(0, positions.length * 2 + 1);
    line.geometry.setPositions(positions);
    line.geometry.computeBoundingBox();
    line.geometry.computeBoundingSphere();
    line.updateMorphTargets();
    line.updateMatrixWorld();
    line.updateMatrix();
    line.computeLineDistances();
    line.material.resolution.set(window.innerWidth, window.innerHeight);  // Add this line to update material resolution
  }, [line]);

  const completeDrawing = useCallback((faceNormal: THREE.Vector3) => {
    setCutStep(CutStep.TRANSFORM);
    if (cameraControls) cameraControls.enabled = true;
    points.current = movePointsAwayFromPlane(points.current.slice(0, points.current.length - 1), -wallThickness.current);
    const shape = generatePrismCutShape(points.current.slice(0, points.current.length - 1), wallThickness.current);

    addCutShape(shape);
    points.current = [];
    line.geometry.dispose();
    updateLineGeometry();
    setIsDragging(false);
  }, [setCutStep, cameraControls, addCutShape, setIsDragging, line, updateLineGeometry]);

  const onKeyUp = useCallback(
    (event: KeyboardEvent) => {
      if (event.key === 'Escape' && cutStep === CutStep.DRAWING) {
        // Reset drawing state
        points.current = [];
        setIsDragging(false);
        updateLineGeometry();
        line.geometry.dispose();
        setCutStep(CutStep.NONE);
        if (cameraControls) cameraControls.enabled = true;
      }
    },
    [cutStep, setCutStep, cameraControls, setIsDragging, line, updateLineGeometry]
  );

  const onMouseDown = useCallback(
    (event: MouseEvent) => {
      if (!selectedItem) return;
      raycaster.setFromCamera(getPointFromMouseEvent(event, gl), camera);
      const intersect = raycaster.intersectObject(selectedItem, true)[0];
      if (!intersect) return;
      const faceNormal = intersect.face?.normal
        .clone()
        .applyMatrix3(new THREE.Matrix3().getNormalMatrix(intersect.object.matrixWorld))
        .normalize();
      if (!faceNormal) return;
      if (cameraControls && cutStep === CutStep.START) {
        setIsDragging(true);
        setCutStep(CutStep.DRAWING);
        const moveDistance = 5 * (selectedItem.userData.wallType === ThreeWallType.INNER ? 1 : -1); // Distance from the face
        const originPos = new THREE.Vector3();
        const newCameraPos = new THREE.Vector3().addVectors(intersect.point, faceNormal.multiplyScalar(moveDistance));
        cameraControls.getPosition(originPos);
        const animateCamera = (fromPosition: THREE.Vector3, toPosition: THREE.Vector3, target: THREE.Vector3) => {
          if (!cameraControls) return;
          const duration = 1000;
          const startTime = performance.now();

          const animate = (time: number) => {
            const elapsed = time - startTime;
            const t = elapsed / duration;

            if (t < 1) {
              const x = fromPosition.x + t * (toPosition.x - fromPosition.x);
              const y = fromPosition.y + t * (toPosition.y - fromPosition.y);
              const z = fromPosition.z + t * (toPosition.z - fromPosition.z);
              cameraControls.setPosition(x, y, z);
              cameraControls.setTarget(target.x, target.y, target.z);
              requestAnimationFrame(animate);
            } else {
              cameraControls.setPosition(toPosition.x, toPosition.y, toPosition.z);
              cameraControls.setTarget(target.x, target.y, target.z);
              cameraControls.enabled = false;
            }
          };

          requestAnimationFrame(animate);
        };
        animateCamera(originPos, newCameraPos, intersect.point);
      }
      if (cutStep === CutStep.DRAWING) {
        let p = drawingEndPoint.current;
        if (points.current.length === 0) {
          p = new THREE.Vector3().addVectors(intersect.point, faceNormal.multiplyScalar(substraction.current * (selectedItem.userData.wallType === ThreeWallType.INNER ? 1 : -1)));
          drawingStartPoint.current = p;
        }
        points.current.push(p);
        if (points.current.length > 3 && points.current[0].distanceTo(points.current[points.current.length - 1]) === 0) {
          completeDrawing(faceNormal);
        }
      }
      if (cutStep === CutStep.NONE) {
        points.current = [];
        line.geometry.dispose();
      }

      updateLineGeometry();
    },
    [camera, gl, raycaster, cameraControls, cutStep, setCutStep, selectedItem, setIsDragging, completeDrawing, updateLineGeometry, line]
  );

  const onMouseMove = useCallback(
    (event: MouseEvent) => {
      if (cutStep === CutStep.DRAWING) {  // Add lineDraging.current check
        if (!selectedItem) return;
        raycaster.setFromCamera(getPointFromMouseEvent(event, gl), camera);
        const intersect = raycaster.intersectObject(selectedItem, true)[0];
        if (!intersect) return;
        if (points.current.length === 0) return;
        const faceNormal = intersect.face?.normal
          .clone()
          .applyMatrix3(new THREE.Matrix3().getNormalMatrix(intersect.object.matrixWorld))
          .normalize();
        if (!faceNormal) return;
        const p = new THREE.Vector3().addVectors(intersect.point, faceNormal.multiplyScalar(substraction.current * (selectedItem.userData.wallType === ThreeWallType.INNER ? 1 : -1)));
        drawingEndPoint.current = projectPointOntoPlane(p, intersect.point, drawingStartPoint.current, p);
        if (getDistanceFromVector3(drawingStartPoint.current, drawingEndPoint.current) < 0.5) {
          drawingEndPoint.current = drawingStartPoint.current;
        }
        points.current = points.current.length === 1 ? points.current : points.current.slice(0, points.current.length - 1);
        points.current.push(drawingEndPoint.current);
        updateLineGeometry();
      }
    },
    [cutStep, selectedItem, raycaster, gl, camera, updateLineGeometry]
  );  

  useEffect(() => {
    gl.domElement.addEventListener('mousedown', onMouseDown);
    gl.domElement.addEventListener('mousemove', onMouseMove);
    window.addEventListener('keyup', onKeyUp);  // Add keyup listener
    return () => {
      gl.domElement.removeEventListener('mousedown', onMouseDown);
      gl.domElement.removeEventListener('mousemove', onMouseMove);
      window.removeEventListener('keyup', onKeyUp);  // Remove keyup listener
    };
  }, [gl, onMouseDown, onMouseMove, onKeyUp]);

  useFrame(() => {
    if (line && points.current.length > 1 && cutStep === CutStep.DRAWING) {
      line.computeLineDistances();
    }
  });

  return (
    <>
      {cutStep === CutStep.DRAWING && (
        <primitive object={line} />
      )}

    </>
  );
};

export default CutItem;
1 Like

When you modify an attribute of a geometry, you have to set the .needsUpdate = true on it for the changes to get pushed to the GPU.

geometry.attributes.position.needsUpdate = true;

Thank you for contributing!
But the result still be the same as previous.
so I updated my code like that.

line.geometry.dispose();
line.geometry = new LineGeometry();
line.geometry.setPositions(positions);

This code works well.
Is my approach correct?

Thank you for contributting again.

When you first create a custom geometry… it hasn’t been uploaded to the GPU, until the first time renderer.render is called.

Once that happens, subsequently modifying the geometry will not update the visible geometry until you explicitly signal the attribute for upload to the GPU with geometry.attributes.position.needsUpdate = true.
(I might be wrong about this, but from my memory this is how it works)

The reason it works like this, is because GPU upload is a relatively costly operation, so if you are doing some kind of incremental modification to an attribute, you often cannot afford for it to automatically update the geometry, and you need to explicitly control that behavior, thus the needsUpdateFlag.

Other than that, your approach looks correct?

If that doesn’t work, then perhaps it’s a good idea to throw your code into a codepen or similar and we can take a deeper look at it.

Thanks for clarifying.
Anyway, if dispose the geometry and then recreate it, the result is good.
I think dispose method free the memory so there is other problem.
:grinning:
so I am going to choose this way.
Thanks again for your contributing and please let me know if there is any appoach than this.
:+1:

1 Like

yeah your approach sounds good.

It won’t become an issue unless you have LOTs of lines in which case rebuilding the entire thing with every change might become cumbersome. But if you hit that point, you can just switch to using the in place .needsUpdate

I just wanted to explain what’s going on under the hood.

1 Like