Vue+Tres+Three.js 오브젝트 없는 곳 클릭했을 때 TransformControls 비활성화

LeeWonjin·2024년 8월 20일
2

문제해결

목록 보기
23/23
post-custom-banner

상황

  • 오브젝트(모델, 매쉬)를 클릭했을 때,
    • 해당 오브젝트에 대한 TransformControls가 활성화된다. (해당 오브젝트를 이동시키는 UI가 표시된다.)
  • 볼 일 다 보고 나서 오브젝트 없는 캔버스의 빈 공간을 마우스 좌클릭하면
    • TransformControls를 비활성화 하고싶다. (오브젝트 이동 UI가 사라졌으면 좋겠다.)

여기서 캔버스의 빈 공간을 클릭했을 때의 이벤트를 받고싶었는데, 특별히 이 때만을 위해 구현된 것은 없었다.

그래서 요구사항은

오브젝트의 Transform(Translate, Rotation, Scale)을 만지는 동안에는 TransformControls가 활성화되어야 한다. 꺼지면 안된다.

  • 그러면서도 동시에 월드 컨트롤(Orbit, Map Control)은 잘 동작해야 한다.
  • 물론 오브젝트의 TransformControls와 충돌하면 안된다. (둘 중 하나만 동작)

오브젝트의 Transform을 만지고 있다는 것은

  • 오브젝트, TransformControls 둘 중 하나를 클릭했을 때
  • 오브젝트의 TransformControls를 조작했을 때
    • Mouse Up 시점에 마우스커서가 오브젝트나 controls의 범위를 벗어났더라도 조작한다고 봄.

만지고 있지 않다는 것은

  • 위 나열한 조건을 피하면서 캔버스의 빈 공간을 클릭했을 때
  • 캔버스 빈공간 드래그는 만지고 있다고 본다. 계속 조작할 확률이 높으니까.
    • World(전체 씬)을 이동해서 계속 Translate를 고칠 가능성이 높음.

시도

Tres.js 샘플 에디터의 코드를 베껴오자

https://lab.tresjs.org/experiments/simple-editor

아쉽게도 오브젝트 선택 해제 자체가 구현되어있지 않다.
배신감을 느끼며 창을 닫는다.
곧 제가 풀리퀘스트를 올려드리겠습니다.

Three.js 샘플 에디터의 코드를 베껴오자

https://threejs.org/editor/

상황이 Tres.js보다는 좀 낫다
오브젝트 바깥을 클릭하면 이동모드가 해제된다.
TransformContorls를 드래그해서 커서가 오브젝트 밖으로 나가도 오브젝트 이동모드가 유지된다.

그런데?

  • 오브젝트 밖에 삐져나온 기즈모를 그냥 드래그없이 클릭하면
  • 컨트롤 UI가 사라진다.
  • 이럼 안된다구

포인터가 빗나갔을 때를 사용하자 (행복회로)

Tres.js의 @pointer-missed이벤트를 TresCanvas에 먹인다.
오브젝트를 클릭하지 않은 경우 이벤트핸들러가 호출된다.

  • 핸들러에서 마우스 오른쪽클릭 했을 때 (event.button===2) early-return시키고
  • 그 다음 줄에 '빈공간 마우스 좌클릭'상황에 해야 할 내용을 작성한다.
<Suspense>
	<TresCanvas class="tres-canvas" @pointer-missed="onEmptySpaceClicked">
    	<TransformControls ... />
        <primitive ... />
        <primitive ... />
    </TresCanvas>
</Suspense>
function onEmptySpaceClicked(ev) {
  // right click
  if (ev.event.button === 2) return;

  // others
  transformTarget.value = null;
}

하지만 안됐죠? (날먹실패)

일반적으로는 잘 된다. 그러나 모든 요구사항을 만족시키지 못함.

해결

온갖 구멍을 다 틀어막아보자

<template>
<Suspense>
  <TresCanvas
    class="tres-canvas"
    clear-color="#333333"
    @mouseup="onCanvasMouseup"
    @mousedown="onCanvasMousedown"
  >
    <TresPerspectiveCamera ref="pCameraRef" :position="[0, 5, 10]" />
    <MapControls make-default :enablePan="!isDraggingModel" />
    <TransformControls
      ref="transformControlsRef"
      v-if="transformTarget"
      ...
      @mouse-down="isDraggingModel = true"
    />
    ...
    <primitive
      ...
      @pointer-down="onModelMousedown"
      @pointer-up="onModelMouseup"
    />

    <primitive
      ...
      @pointer-down="onModelMousedown"
      @pointer-up="onModelMouseup"
    />
    ...
  </TresCanvas>
</Suspense>
</template>

<script setup>
import { ref, onMounted, shallowRef } from "vue";

// CANVAS
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { TresCanvas } from "@tresjs/core";
import { MapControls, TransformControls } from "@tresjs/cientos";

// transform control
const transformControlsRef = ref(null);
const transformMode = ref("translate");
const transformTarget = ref(null);
const isDraggingModel = ref(false); // model을 드래깅중이거나 transformControl을 조작중인지 여부

function onModelMousedown(ev) {
  ev.stopPropagation();
  isDraggingModel.value = true;

  if (ev && ev.eventObject) {
    // transform control 제어 대상 오브젝트 결정
    transformTarget.value = ev.eventObject.uuid;
  }
}

function onModelMouseup(ev) {
  ev.stopPropagation();
  isDraggingModel.value = false;
}

// disable transform controls when empty space clicked
const pCameraRef = ref(null);
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const mouseCoords = {
  downX: 0,
  downY: 0,
  upX: 0,
  upY: 0,
};

function onCanvasMousedown(ev) {
  mouseCoords.downX = ev.clientX;
  mouseCoords.downY = ev.clientY;
}

function onCanvasMouseup(ev) {
  // 오브젝트 이동모드 아니면 무시
  if (!transformTarget.value) {
    return;
  }
  // 오브젝트 이동(transformControl 조작)하는 중이었으면 무시
  else if (isDraggingModel.value) {
    isDraggingModel.value = false;
    return;
  }
  // 오른쪽 클릭 시 무시
  else if (ev.button === 2) {
    return;
  }

  // 클릭이 아니라 드래그를 했으면 무시
  mouseCoords.upX = ev.clientX;
  mouseCoords.upY = ev.clientY;
  const { downX, downY, upX, upY } = mouseCoords;
  if (Math.abs(downX - upX) > 5 || Math.abs(downY - upY) > 5) {
    return;
  }

  // 현재 마우스 커서와 transformTarget이 겹치면 무시
  // -- 마우스 위치 정규화
  const rect = ev.target.getBoundingClientRect();
  mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
  mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
  // -- intersect확인 (마우스위치와 현재 transformTarget.)
  raycaster.setFromCamera(mouse, pCameraRef.value);
  const targetRef = itemRefs.value[transformTarget.value];
  const intersects = raycaster.intersectObject(targetRef);
  if (intersects.length !== 0) {
    console.log("asdfasdfdfsadfasdfsdafds");
    return;
  }

  // 축하합니다. transformControl을 이제 죽입니다.
  transformTarget.value = null;
}
</script>

profile
노는게 제일 좋습니다.
post-custom-banner

0개의 댓글