From fd9f1b1bf2bfdf016b334884b182e6f5c0187f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 14 Mar 2026 08:42:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[fire-shutter]=203D=20=EB=B7=B0=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EC=85=8B=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=A0=95=EB=A9=B4/=ED=8F=89=EB=A9=B4/=EC=9A=B0?= =?UTF-8?q?=EC=B8=A1/=EC=A2=8C=EC=B8=A1/=EB=B0=B0=EB=A9=B4/=ED=88=AC?= =?UTF-8?q?=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 우상단에 뷰 전환 버튼 패널 배치 - easeOutCubic 애니메이션으로 부드러운 카메라 전환 - 현재 타겟/거리 유지하며 카메라 위치만 변경 --- .../rd/fire-shutter-drawing/index.blade.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/resources/views/rd/fire-shutter-drawing/index.blade.php b/resources/views/rd/fire-shutter-drawing/index.blade.php index 1f9c9706..1e2d8a09 100644 --- a/resources/views/rd/fire-shutter-drawing/index.blade.php +++ b/resources/views/rd/fire-shutter-drawing/index.blade.php @@ -1617,6 +1617,28 @@ function animate() { isoBadge.onclick = () => { fs3dShowAll(); }; container.appendChild(isoBadge); + // ── 뷰 전환 패널 (정면/평면/우측면/좌측면/투시) ── + const viewPanel = document.createElement('div'); + viewPanel.style.cssText = 'position:absolute;top:8px;right:8px;display:flex;gap:4px;z-index:10;'; + const viewPresets = [ + { label: '정면', icon: '⬜', key: 'front' }, + { label: '평면', icon: '⬒', key: 'top' }, + { label: '우측', icon: '▷', key: 'right' }, + { label: '좌측', icon: '◁', key: 'left' }, + { label: '배면', icon: '⬛', key: 'back' }, + { label: '투시', icon: '◈', key: 'persp' }, + ]; + viewPresets.forEach(vp => { + const btn = document.createElement('button'); + btn.style.cssText = 'padding:5px 10px;background:rgba(30,41,59,0.85);border:1px solid rgba(100,116,139,0.4);color:#94a3b8;font-size:11px;font-weight:700;border-radius:6px;cursor:pointer;font-family:Pretendard,sans-serif;transition:all 0.15s;'; + btn.textContent = vp.label; + btn.onmouseenter = () => { btn.style.background = 'rgba(59,130,246,0.3)'; btn.style.color = '#fff'; btn.style.borderColor = '#3b82f6'; }; + btn.onmouseleave = () => { btn.style.background = 'rgba(30,41,59,0.85)'; btn.style.color = '#94a3b8'; btn.style.borderColor = 'rgba(100,116,139,0.4)'; }; + btn.onclick = () => fs3dSetView(vp.key); + viewPanel.appendChild(btn); + }); + container.appendChild(viewPanel); + const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); @@ -2430,6 +2452,38 @@ function fs3dShowAll() { if (badge) badge.style.display = 'none'; } + // 뷰 프리셋 (정면/평면/우측면/좌측면/배면/투시) + function fs3dSetView(preset) { + if (!camera || !controls) return; + const t = controls.target.clone(); // 현재 타겟 유지 + const dist = camera.position.distanceTo(t); // 현재 거리 유지 + const d = Math.max(dist, 2000); // 최소 거리 보장 + let pos; + switch (preset) { + case 'front': pos = new THREE.Vector3(t.x, t.y, t.z + d); break; // +Z: 정면 (전면에서 봄) + case 'back': pos = new THREE.Vector3(t.x, t.y, t.z - d); break; // -Z: 배면 (후면에서 봄) + case 'top': pos = new THREE.Vector3(t.x, t.y + d, t.z + 1); break; // +Y: 평면 (위에서 봄) + case 'right': pos = new THREE.Vector3(t.x + d, t.y, t.z); break; // +X: 우측면 + case 'left': pos = new THREE.Vector3(t.x - d, t.y, t.z); break; // -X: 좌측면 + case 'persp': pos = new THREE.Vector3(t.x + d * 0.7, t.y + d * 0.5, t.z + d * 0.7); break; + default: return; + } + // 부드러운 전환 (애니메이션) + const startPos = camera.position.clone(); + const startTime = performance.now(); + const duration = 400; // ms + function animateView(now) { + const elapsed = now - startTime; + const progress = Math.min(elapsed / duration, 1); + const ease = 1 - Math.pow(1 - progress, 3); // easeOutCubic + camera.position.lerpVectors(startPos, pos, ease); + camera.lookAt(t); + controls.update(); + if (progress < 1) requestAnimationFrame(animateView); + } + requestAnimationFrame(animateView); + } + // 3D Controls window.fs3dShutterPos = function(v) { undoSaveState();