3D House Configurator Mobile Issue(Drag Drop)


I tried implementing drag-and-drop using PointerEvents (onPointerEnter, onPointerMove, onPointerDown, onPointerUp). It works well on desktop, but it’s not functioning properly on mobile—some pointer events seem to be missed. How can I fix this?
the 2D UI is just completely separate from the 3D parts, there’s a big chance r3f onPointer events will not be triggered if an external onPointerDown was first triggered

2 Likes

Perhaps the responsiveness of your site is moving some invisible element in front of your drop target?

1 Like
'use client';
import React, { memo, useCallback, useRef } from 'react';
import DoorMeta from '@/assets/3dObject/DoorMeta.json';
import { useDraggable } from '@/store/useDraggable';
import DoorImage, { DoorItem } from '@/components/DoorImage';

const DoorList: React.FC = () => {
  const { setDragItem, setDragPosition } = useDraggable();
  const parentRef = useRef<HTMLDivElement | null>(null);
  const imageRefs = useRef<(HTMLImageElement | null)[]>([]);
  const dragOffset = useRef({ x: 0, y: 0 });

  const handlePointerDown = useCallback(
    (e: React.PointerEvent<HTMLImageElement>, index: number, item: DoorItem) => {
      const ref = imageRefs.current[index];
      const parent = parentRef.current;
      if (!ref || !parent) return;

      const offsetX = e.clientX - ref.getBoundingClientRect().left;
      const offsetY = e.clientY - ref.getBoundingClientRect().top;

      dragOffset.current = { x: offsetX, y: offsetY };


      setDragItem({
        src: item.image,
        category: index,
        type: item.name,
        url: item.model,
        height: item.height,
        width: item.width,
      });
      // const myCanvas = document.getElementById("myCanvas");
      // if (myCanvas)
      //   myCanvas.dispatchEvent(new PointerEvent('pointermove', {
      //     bubbles: true, cancelable: true,
      //     clientX: e.clientX,
      //     clientY: e.clientY,
      //   }));
      document.addEventListener('pointermove', handlePointerMove);
      document.addEventListener('pointerup', handlePointerUp);
      // document.addEventListener('touchend', handlePointerUp);
      // document.addEventListener('touchcancel', handlePointerUp);
    },
    [setDragItem]
  );

  const handlePointerMove = useCallback(
    (e: PointerEvent) => {
      const parentRect = parentRef.current?.getBoundingClientRect();
      if (!parentRect) return;

      setDragPosition({
        x: e.clientX - dragOffset.current.x,
        y: e.clientY + scrollY - dragOffset.current.y
      });

    },
    [setDragPosition]
  );

  const handlePointerUp = useCallback(() => {
    setDragPosition(null);
    setDragItem(null);
    document.removeEventListener('pointermove', handlePointerMove);
    document.removeEventListener('pointerup', handlePointerUp);
    // document.removeEventListener('touchend', handlePointerUp);
    // document.removeEventListener('touchcancel', handlePointerUp);
  }, [setDragItem, setDragPosition, handlePointerMove]);

  return (
    <div
      ref={parentRef}
      className="flex flex-row gap-4 p-4 bg-white overflow-x-auto w-full"
    >
      {(DoorMeta as DoorItem[]).map((item, index) => (
        <DoorImage
          key={index}
          item={item}
          index={index}
          onPointerDown={handlePointerDown}
          setRef={(el: HTMLImageElement | null) => {
            imageRefs.current[index] = el;
          }}
        />
      ))}
    </div>
  );
};

export default memo(DoorList);

/* eslint-disable @next/next/no-img-element */
'use client'

import { useDraggable } from '@/store/useDraggable'
import { memo } from 'react'
import { useShallow } from 'zustand/react/shallow'

const DraggableImage = () => {
    const { dragItem, dragPosition, isOnWall } = useDraggable(
        useShallow((state) => ({
            dragItem: state.dragItem,
            dragPosition: state.dragPosition,
            isOnWall: state.isOnWall,
        }))
    );

    return dragItem && dragPosition && !isOnWall && (
        <img
            src={dragItem.src}
            alt={dragItem.type}
            id={dragItem.type + dragItem.category}
            style={{
                position: 'absolute',
                left: dragPosition.x,
                top: dragPosition.y,
                opacity: 0.8,
                pointerEvents: 'none',
                userSelect: 'none',
                cursor: 'grabbing',
                zIndex: 50,
            }}
        />
    );
}

export default memo(DraggableImage)
/* eslint-disable @typescript-eslint/no-explicit-any */
import { memo, useCallback, useEffect, useMemo, useRef } from "react"
import * as THREE from "three"
import PanelDimensions from "@/assets/PanelDimension.json"
import ExtrudeSettings from "@/assets/ExtrudeSetting.json"
import { useDraggable } from "@/store/useDraggable"
import { useShallow } from "zustand/react/shallow"
import { ThreeEvent, useFrame } from "@react-three/fiber"
import { useMaterialStore } from "@/store/useMaterialStore"
import { throttle } from 'lodash'
import { useWallModuleCalculator } from "@/utils/useWallModuleCalculator"
// import DoorList from "@/assets/3dObject/DoorMeta.json"

import BattenDimension from "@/assets/BattenDimesion.json"

interface PanelProps {
  wallWidth: number
  eaveHeight: number
  rotation: number[]
  position: number[]
  planePos: number[]
  planeRotY: number,
  label: string
}

type CategorizedModel = {
  key: number;
  position: number;
};

const Panel = ({
  wallWidth,
  rotation,
  position,
  eaveHeight,
  planePos,
  planeRotY,
  label
}: PanelProps) => {
  const modelList = useRef<any>({
    window: [],
    doorM12: [],
    doorM25: [],
    doorM30: [],
    doorM50: [],
  });
  const moduleList = useRef<any>([]);


  const [
    dragItem,
    objectPosition,
    isOnWall,
    alignModelList,
    addAlignModel,
    setObjectPosition,
    setIsOnWall,
    setRotY,
  ] = useDraggable(useShallow((state) => [
    state.dragItem,
    state.objectPosition,
    state.isOnWall,
    state.alignModelList,
    state.addAlignModel,
    state.setObjectPosition,
    state.setIsOnWall,
    state.setRotY,
  ]))



  const handlePointerMove = (e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation()
  }

  const handlePointerEnter = (e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation()
    console.log('Pointer entered mesh:', label);
    if (!dragItem) return

    setRotY(planeRotY)
    setIsOnWall(true)
  }

  useEffect(() => {
    if (!dragItem) return;
    const categorizedModels: Record<string, CategorizedModel[]> = {
      window: [],
      doorM12: [],
      doorM25: [],
      doorM30: [],
      doorM50: [],
    }
    const tempList = [...alignModelList.models]
    // if (updateDrag > -1)
    //   tempList = tempList.filter((_, index) => (index !== updateDrag))
    tempList.forEach((item, i) => {
      if (item.label !== label) return

      const pos = (label === 'front' || label === 'back') ? item.position[0] : item.position[2]
      categorizedModels[item.type]?.push({
        key: i,
        position: wallWidth / 2 + pos
      })
    })
    modelList.current = categorizedModels
  }, [dragItem])

  const handlePointerLeave = (e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation()
    setIsOnWall(false)
    if (dragItem) {
      setObjectPosition(null)
    }
  }

  const handlePointerUp = (e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation()
    if (dragItem && isOnWall && objectPosition) {
      const collector: any = []
      moduleList.current.map((item: any) => {
        const pos = (label === "front") ? item.Pos - wallWidth / 2 : ((label === "back") ? -item.Pos + wallWidth / 2 : ((label === "right") ? item.Pos - wallWidth / 2 : -item.Pos + wallWidth / 2))
        collector.push([pos, item.Size, label, item.Module])
      })
      const updates = moduleList.current
        .filter((ele: any) => ele.Contain !== '')
        .map((ele: any) => ({
          type: label,
          pos: ele.Pos - wallWidth / 2,
          index: ele.key ?? 0,
        }));
      addAlignModel({
        model: {
          ...dragItem,
          position: objectPosition,
          rotation: [0, planeRotY - Math.PI / 2, 0],
          scale: [1, 1, 1],
          label: label
        },
        collector: collector,
        updates: updates
      })
    } else
      setIsOnWall(false)
  }



  return (

      <mesh
        ref={meshRef}
        onPointerMove={handlePointerMove}
        onPointerEnter={handlePointerEnter}
        onPointerLeave={handlePointerLeave}
        onPointerOut={handlePointerLeave}
        onPointerUp={handlePointerUp}
        rotation={[0, planeRotY, 0]}
        position={[planePos[0], planePos[1], planePos[2]]}
      >
        <planeGeometry args={[wallWidth, eaveHeight]} />
        <meshBasicMaterial color="white" side={THREE.DoubleSide} transparent opacity={0} />
      </mesh>
  )
}

export default memo(Panel)```

Also try asking here! :smiley: Poimandres

1 Like

Try

#root {
    touch-action: none;
}

in your .css

Or you can set ontouchstart = e=>e.preventDefault (or e=>false)