Draw polygons/lines and attach labels

Hi, I must confess this is my first approach to canvas drawing. I developed a web app in React+Typescript and need to allow to the user to draw a shape on google maps and show him the lenght of the lines. I have a recorded video of a web site which already does it:

draw_gm

I used the google maps api for static images in order to get the map and put it into a canvas. I have been able to add a zoom and draw the lines using the following code:

import { FC } from "react";
import * as THREE from "three";

export type CustomLine = {
  start: THREE.Vector3;
  end: THREE.Vector3;
  distance: number;
};

const Lines: FC<{ lines: CustomLine[]; position?: THREE.Vector3 }> = ({ lines, position }) => {
  return (
    <group
      position={position} // Example position adjustment
    >
      {/* Render finalized lines */}
      {lines.map((line, index) => (
        <lineSegments key={index}>
          <bufferGeometry>
            <bufferAttribute
              attach="attributes-position"
              array={new Float32Array([line.start.x, line.start.y, line.start.z, line.end.x, line.end.y, line.end.z])}
              count={2}
              itemSize={3}
            />
          </bufferGeometry>
          <lineBasicMaterial color="red" linewidth={2} />
        </lineSegments>
      ))}
    </group>
  );
};

export default Lines;

I am asking for help to reproduce a similar output. Or at least some hints :slight_smile:

more or less you should be able to re-use most of the code you already have written. drei has some good abstractions around lines, labels, text. drei/line for instance. in fiber events are the same as pointer events on the dom, so you can capture events on pointer-over and release on pointer-out, same as your canvas code does it.

https://codesandbox.io/p/sandbox/draggable-lines-yuyr6z

Thanks,

I have a question before I continue. I created a Canvas:

<Canvas orthographic>
    <ZoomController zoom={50} />

    <ambientLight intensity={0.5} />

    {/* Zoom Layer */}
    <OrbitControls enableZoom={true} zoomSpeed={2} enableRotate={false} />

    {/* Moving Layer */}
    <MovingImage geometry={geometry} texture={texture} isDraggable={isDraggable} setPosition={setOffset} />

    {/* Drawing Layer */}
    {!isDraggable && <DrawingLayer setLines={setLines} lines={lines} offset={offset} />}

    {/* Lines Layer (Group all lines together) */}
    <Lines lines={lines} position={offset} />
</Canvas>

I am loading an image and putting it in a and added logic in order to move it using the mouse:


const MovingImage: React.FC<{
  geometry: number;
  texture: THREE.Texture;
  isDraggable: boolean;
  setPosition: (position: THREE.Vector3) => void;
}> = ({ geometry, texture, isDraggable, setPosition }) => {
  const { raycaster, camera, gl } = useThree();
  const [dragging, setDragging] = useState(false);
  const meshRef = useRef<THREE.Mesh>(null);
  const [offset, setOffset] = useState(new THREE.Vector3());

  const onPointerDown = (event: PointerEvent) => {
    if (!meshRef.current || !isDraggable) return;

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

    raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);

    const planeIntersectPoint = new THREE.Vector3();
    if (raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), -0), planeIntersectPoint)) {
      setDragging(true);
      setOffset(meshRef.current.position.clone().sub(planeIntersectPoint));
    }
  };

  const onPointerMove = (event: PointerEvent) => {
    if (!dragging || !meshRef.current) return;

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

    raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);

    const planeIntersectPoint = new THREE.Vector3();
    if (raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), -0), planeIntersectPoint)) {
      const position = planeIntersectPoint.add(offset);
      meshRef.current.position.copy(position);
      setPosition(position);
    }
  };

  const onPointerUp = () => setDragging(false);

  useEffect(() => {
    gl.domElement.addEventListener("pointerdown", onPointerDown);
    gl.domElement.addEventListener("pointermove", onPointerMove);
    gl.domElement.addEventListener("pointerup", onPointerUp);

    return () => {
      gl.domElement.removeEventListener("pointerdown", onPointerDown);
      gl.domElement.removeEventListener("pointermove", onPointerMove);
      gl.domElement.removeEventListener("pointerup", onPointerUp);
    };
  }, [dragging, isDraggable]);

  return (
    <mesh ref={meshRef}>
      <planeGeometry args={[geometry, geometry]} />
      <meshBasicMaterial map={texture} />
    </mesh>
  );
};

Currently it works, but as you can see in the MovingImage I have to call back the offset, which I need to pass to the lines in order that the lines move along the image. Here the difference:

vs

I was wondering if I need to implement the movement by myself or if there is something built in (did not find it). Furhtermore, if there is a better way to move the lines layer along the background image.

you don’t need to register pointer events, like i said this is all inbuilt. i wouldn’t register pointer events unless absolutely needed and in 99.9% of all cases you won’t have to.

<mesh onPointerDown={e => {
  e.stopPropagation()
  e.target.setPointerCapture(e.pointerId)
}}
onPointerUp={e => {
  e.target.releasePointerCapture(e.pointerId)
}}
onPointerMove={e => ...} 

you don’t need to calculate mouseX and Y, this is part of state

const pointer = useThree(state => state.pointer)
// pointer.x // in three coordinates
// pointer.y // in three coordinates

you don’t need to intersect anything because that’s already done Events - React Three Fiber

you don’t need to mess with lineSegments, drei/line is easier to use

the sandbox i posted above shows you how to much something underneath the cursor

onPointerMove={e => {
  setPosition(e.unprojectedPoint)

if you want the lines to move alone just parent them into the mesh, where the mesh goes the lines will follow. let threejs do the work.

Ok I got it.
I have just to understand why Typescript does not recognize setPointerCapture on event.target.

I fear I am missing something.

I think the main problem is that the main mesh with the map texture, has to be zoomable, so I don’t relly understand how to set up the map layer and the lines drawable layer.

Actually, the code I developed works, but I fear it is not a really good code and could lead to problems (complexity) once I need more details to draw.

Basically, I need:

  • A layer with the map texture
  • A layer where the user can draw
  • Both layer are movable, together (lines haw to stay on texture position), as well when zooming