From ae37abd6630ed651fddddf69a044bcc24434d6dd Mon Sep 17 00:00:00 2001 From: shumoLR <328906329@qq.com> Date: Mon, 12 Jan 2026 15:44:22 +0800 Subject: [PATCH] Delete js directory --- js/Multiangle_Camera.js | 1023 --------------------------------------- 1 file changed, 1023 deletions(-) delete mode 100644 js/Multiangle_Camera.js diff --git a/js/Multiangle_Camera.js b/js/Multiangle_Camera.js deleted file mode 100644 index 76120f6..0000000 --- a/js/Multiangle_Camera.js +++ /dev/null @@ -1,1023 +0,0 @@ -import { app } from "../../../scripts/app.js"; -import { api } from "../../../scripts/api.js"; -import * as THREE from "https://esm.sh/three@0.170.0"; - -// ============== Styles ============== -const WIDGET_STYLES = ` - .synvow-multiangle-container { - width: 100%; - height: 100%; - position: relative; - background: #0a0a0f; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - border-radius: 8px; - overflow: hidden; - } - .synvow-multiangle-canvas { - width: 100%; - height: 100%; - display: block; - } - .synvow-multiangle-prompt { - position: absolute; - top: 8px; - left: 8px; - right: 8px; - background: rgba(10, 10, 15, 0.9); - border: 1px solid rgba(233, 61, 130, 0.3); - border-radius: 6px; - padding: 6px 10px; - font-size: 11px; - color: #E93D82; - backdrop-filter: blur(4px); - font-family: 'Consolas', 'Monaco', monospace; - word-break: break-all; - line-height: 1.4; - } - .synvow-multiangle-info { - position: absolute; - bottom: 8px; - left: 8px; - right: 8px; - background: rgba(10, 10, 15, 0.9); - border: 1px solid rgba(233, 61, 130, 0.3); - border-radius: 6px; - padding: 8px 12px; - font-size: 11px; - color: #e0e0e0; - display: flex; - justify-content: space-around; - align-items: center; - backdrop-filter: blur(4px); - } - .synvow-multiangle-param { text-align: center; } - .synvow-multiangle-param-label { - color: #888; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; - } - .synvow-multiangle-param-value { font-weight: 600; font-size: 13px; } - .synvow-multiangle-param-value.azimuth { color: #E93D82; } - .synvow-multiangle-param-value.elevation { color: #00FFD0; } - .synvow-multiangle-param-value.zoom { color: #FFB800; } - .synvow-multiangle-reset { - width: 24px; - height: 24px; - border-radius: 4px; - border: 1px solid rgba(233, 61, 130, 0.4); - background: rgba(10, 10, 15, 0.8); - color: #E93D82; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - transition: all 0.2s ease; - flex-shrink: 0; - } - .synvow-multiangle-reset:hover { - background: rgba(233, 61, 130, 0.2); - border-color: #E93D82; - } - .synvow-multiangle-dropdowns { - position: absolute; - top: 40px; - left: 8px; - display: flex; - flex-direction: column; - gap: 4px; - z-index: 10; - } - .synvow-multiangle-dropdown { display: flex; align-items: center; gap: 6px; } - .synvow-multiangle-dropdown-label { - font-size: 9px; - color: #888; - text-transform: uppercase; - letter-spacing: 0.5px; - min-width: 28px; - text-align: right; - white-space: nowrap; - } - .synvow-multiangle-dropdown-label.azimuth { color: #E93D82; } - .synvow-multiangle-dropdown-label.elevation { color: #00FFD0; } - .synvow-multiangle-dropdown-label.distance { color: #FFB800; } - .synvow-multiangle-select { - background: rgba(10, 10, 15, 0.9); - border: 1px solid rgba(100, 100, 120, 0.4); - border-radius: 4px; - padding: 3px 6px; - font-size: 10px; - color: #e0e0e0; - cursor: pointer; - outline: none; - min-width: 120px; - backdrop-filter: blur(4px); - } - .synvow-multiangle-select:hover { border-color: rgba(150, 150, 170, 0.6); } - .synvow-multiangle-select:focus { border-color: #E93D82; } - .synvow-multiangle-select option { background: #1a1a2e; color: #e0e0e0; } - .synvow-multiangle-upload-btn { - width: 24px; - height: 24px; - border-radius: 4px; - border: 1px solid rgba(100, 180, 255, 0.4); - background: rgba(10, 10, 15, 0.8); - color: #64B4FF; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - transition: all 0.2s ease; - flex-shrink: 0; - margin-right: 4px; - } - .synvow-multiangle-upload-btn:hover { - background: rgba(100, 180, 255, 0.2); - border-color: #64B4FF; - } - .synvow-multiangle-file-input { display: none; } -`; - -function injectStyles() { - if (document.getElementById('synvow-multiangle-styles')) return; - const style = document.createElement('style'); - style.id = 'synvow-multiangle-styles'; - style.textContent = WIDGET_STYLES; - document.head.appendChild(style); -} - -// ============== Translations ============== -const translations = { - horizontal: 'H', - vertical: 'V', - zoom: 'Z', - horizontalFull: 'HORIZONTAL', - verticalFull: 'VERTICAL', - zoomFull: 'ZOOM', - resetToDefaults: 'Reset to defaults', - loadImage: 'Load image', - frontView: 'front view', - frontRightQuarterView: 'front-right quarter view', - rightSideView: 'right side view', - backRightQuarterView: 'back-right quarter view', - backView: 'back view', - backLeftQuarterView: 'back-left quarter view', - leftSideView: 'left side view', - frontLeftQuarterView: 'front-left quarter view', - lowAngleShot: 'low-angle shot', - eyeLevelShot: 'eye-level shot', - elevatedShot: 'elevated shot', - highAngleShot: 'high-angle shot', - wideShot: 'wide shot', - mediumShot: 'medium shot', - closeUp: 'close-up' -}; - -function t(key) { - return translations[key] || key; -} - -// ============== CameraWidget Class ============== -class CameraWidget { - constructor(options) { - injectStyles(); - - this.container = options.container; - this.onStateChange = options.onStateChange; - this.state = { - azimuth: options.initialState?.azimuth ?? 0, - elevation: options.initialState?.elevation ?? 0, - distance: options.initialState?.distance ?? 5, - imageUrl: null, - uploadedImageFile: null - }; - this.node = options.node; - - this.liveAzimuth = this.state.azimuth; - this.liveElevation = this.state.elevation; - this.liveDistance = this.state.distance; - - this.CENTER = new THREE.Vector3(0, 0.5, 0); - this.AZIMUTH_RADIUS = 1.8; - this.ELEVATION_RADIUS = 1.4; - this.ELEV_ARC_X = -0.8; - - this.isDragging = false; - this.dragTarget = null; - this.hoveredHandle = null; - this.raycaster = new THREE.Raycaster(); - this.mouse = new THREE.Vector2(); - this.animationId = null; - this.time = 0; - this.needsRender = true; - - this.AZIMUTH_OPTIONS = [ - { key: 'frontView', value: 0 }, - { key: 'frontRightQuarterView', value: 45 }, - { key: 'rightSideView', value: 90 }, - { key: 'backRightQuarterView', value: 135 }, - { key: 'backView', value: 180 }, - { key: 'backLeftQuarterView', value: 225 }, - { key: 'leftSideView', value: 270 }, - { key: 'frontLeftQuarterView', value: 315 } - ]; - - this.ELEVATION_OPTIONS = [ - { key: 'lowAngleShot', value: -30 }, - { key: 'eyeLevelShot', value: 0 }, - { key: 'elevatedShot', value: 30 }, - { key: 'highAngleShot', value: 60 } - ]; - - this.DISTANCE_OPTIONS = [ - { key: 'wideShot', value: 1 }, - { key: 'mediumShot', value: 4 }, - { key: 'closeUp', value: 8 } - ]; - - this.createDOM(); - this.initThreeJS(); - this.bindEvents(); - this.updateDisplay(); - this.animate(); - } - - createDOM() { - const azimuthOptionsHtml = this.AZIMUTH_OPTIONS.map(opt => - `` - ).join(''); - const elevationOptionsHtml = this.ELEVATION_OPTIONS.map(opt => - `` - ).join(''); - const distanceOptionsHtml = this.DISTANCE_OPTIONS.map(opt => - `` - ).join(''); - - this.container.innerHTML = ` -
-
-
<sks> front view eye-level shot medium shot
-
-
- ${t('horizontal')} - -
-
- ${t('vertical')} - -
-
- ${t('zoom')} - -
-
-
-
-
${t('horizontalFull')}
-
0°
-
-
-
${t('verticalFull')}
-
0°
-
-
-
${t('zoomFull')}
-
5.0
-
- - - -
-
- `; - - const containerEl = this.container.querySelector('.synvow-multiangle-container'); - this.canvasContainer = containerEl.querySelector('.synvow-multiangle-canvas'); - this.promptEl = containerEl.querySelector('.synvow-multiangle-prompt'); - this.hValueEl = containerEl.querySelector('.synvow-multiangle-param-value.azimuth'); - this.vValueEl = containerEl.querySelector('.synvow-multiangle-param-value.elevation'); - this.zValueEl = containerEl.querySelector('.synvow-multiangle-param-value.zoom'); - this.azimuthSelect = containerEl.querySelector('.synvow-multiangle-select.azimuth'); - this.elevationSelect = containerEl.querySelector('.synvow-multiangle-select.elevation'); - this.distanceSelect = containerEl.querySelector('.synvow-multiangle-select.distance'); - - containerEl.querySelector('.synvow-multiangle-reset').addEventListener('click', () => this.resetToDefaults()); - - this.fileInput = containerEl.querySelector('.synvow-multiangle-file-input'); - containerEl.querySelector('.synvow-multiangle-upload-btn').addEventListener('click', () => this.fileInput.click()); - this.fileInput.addEventListener('change', (e) => this.handleFileUpload(e)); - - this.azimuthSelect.addEventListener('change', () => { - const value = parseInt(this.azimuthSelect.value, 10); - this.state.azimuth = value; - this.liveAzimuth = value; - this.updateVisuals(); - this.updateDisplay(); - this.notifyStateChange(); - }); - - this.elevationSelect.addEventListener('change', () => { - const value = parseInt(this.elevationSelect.value, 10); - this.state.elevation = value; - this.liveElevation = value; - this.updateVisuals(); - this.updateDisplay(); - this.notifyStateChange(); - }); - - this.distanceSelect.addEventListener('change', () => { - const value = parseInt(this.distanceSelect.value, 10); - this.state.distance = value; - this.liveDistance = value; - this.updateVisuals(); - this.updateDisplay(); - this.notifyStateChange(); - }); - } - - initThreeJS() { - const width = this.canvasContainer.clientWidth || 300; - const height = this.canvasContainer.clientHeight || 300; - - this.scene = new THREE.Scene(); - this.scene.background = new THREE.Color(0x0a0a0f); - - this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); - this.camera.position.set(4, 3.5, 4); - this.camera.lookAt(0, 0.3, 0); - - this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); - this.renderer.setSize(width, height); - this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - this.renderer.outputColorSpace = THREE.SRGBColorSpace; - this.canvasContainer.appendChild(this.renderer.domElement); - - this.scene.add(new THREE.AmbientLight(0xffffff, 0.4)); - - const mainLight = new THREE.DirectionalLight(0xffffff, 0.8); - mainLight.position.set(5, 10, 5); - this.scene.add(mainLight); - - const fillLight = new THREE.DirectionalLight(0xE93D82, 0.3); - fillLight.position.set(-5, 5, -5); - this.scene.add(fillLight); - - this.gridHelper = new THREE.GridHelper(5, 10, 0x1a1a2e, 0x12121a); - this.gridHelper.position.y = -0.01; - this.scene.add(this.gridHelper); - - this.createSubject(); - this.createCameraIndicator(); - this.createAzimuthRing(); - this.createElevationArc(); - this.createDistanceHandle(); - this.updateVisuals(); - } - - createGridTexture() { - const canvas = document.createElement('canvas'); - const size = 128; - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - - ctx.fillStyle = '#1a1a2a'; - ctx.fillRect(0, 0, size, size); - - ctx.strokeStyle = '#2a2a3a'; - ctx.lineWidth = 1; - const gridSize = 16; - for (let i = 0; i <= size; i += gridSize) { - ctx.beginPath(); - ctx.moveTo(i, 0); - ctx.lineTo(i, size); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, i); - ctx.lineTo(size, i); - ctx.stroke(); - } - - const texture = new THREE.CanvasTexture(canvas); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.repeat.set(4, 4); - return texture; - } - - createSubject() { - const cardGeo = new THREE.BoxGeometry(1.2, 1.2, 0.02); - const frontMat = new THREE.MeshBasicMaterial({ color: 0x3a3a4a }); - const backMat = new THREE.MeshBasicMaterial({ map: this.createGridTexture() }); - const edgeMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2a }); - - this.imagePlane = new THREE.Mesh(cardGeo, [edgeMat, edgeMat, edgeMat, edgeMat, frontMat, backMat]); - this.imagePlane.position.copy(this.CENTER); - this.scene.add(this.imagePlane); - this.planeMat = frontMat; - - const frameGeo = new THREE.EdgesGeometry(cardGeo); - this.imageFrame = new THREE.LineSegments(frameGeo, new THREE.LineBasicMaterial({ color: 0xE93D82 })); - this.imageFrame.position.copy(this.CENTER); - this.scene.add(this.imageFrame); - - const glowRingMat = new THREE.MeshBasicMaterial({ color: 0xE93D82, transparent: true, opacity: 0.4, side: THREE.DoubleSide }); - this.glowRing = new THREE.Mesh(new THREE.RingGeometry(0.55, 0.58, 24), glowRingMat); - this.glowRing.position.set(0, 0.01, 0); - this.glowRing.rotation.x = -Math.PI / 2; - this.scene.add(this.glowRing); - } - - createCameraIndicator() { - const camMat = new THREE.MeshBasicMaterial({ color: 0xE93D82 }); - this.cameraIndicator = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.4, 4), camMat); - this.scene.add(this.cameraIndicator); - - const camGlowMat = new THREE.MeshBasicMaterial({ color: 0xff6ba8, transparent: true, opacity: 0.8 }); - this.camGlow = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), camGlowMat); - this.scene.add(this.camGlow); - } - - createAzimuthRing() { - const azRingMat = new THREE.MeshBasicMaterial({ color: 0xE93D82, transparent: true, opacity: 0.7 }); - this.azimuthRing = new THREE.Mesh(new THREE.TorusGeometry(this.AZIMUTH_RADIUS, 0.04, 8, 48), azRingMat); - this.azimuthRing.rotation.x = Math.PI / 2; - this.azimuthRing.position.y = 0.02; - this.scene.add(this.azimuthRing); - - const azHandleMat = new THREE.MeshBasicMaterial({ color: 0xE93D82 }); - this.azimuthHandle = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 12), azHandleMat); - this.scene.add(this.azimuthHandle); - - const azGlowMat = new THREE.MeshBasicMaterial({ color: 0xE93D82, transparent: true, opacity: 0.2 }); - this.azGlow = new THREE.Mesh(new THREE.SphereGeometry(0.22, 8, 8), azGlowMat); - this.scene.add(this.azGlow); - } - - createElevationArc() { - const arcPoints = []; - for (let i = 0; i <= 16; i++) { - const angle = (-30 + (90 * i / 16)) * Math.PI / 180; - arcPoints.push(new THREE.Vector3( - this.ELEV_ARC_X, - this.ELEVATION_RADIUS * Math.sin(angle) + this.CENTER.y, - this.ELEVATION_RADIUS * Math.cos(angle) - )); - } - const arcCurve = new THREE.CatmullRomCurve3(arcPoints); - const elArcMat = new THREE.MeshBasicMaterial({ color: 0x00FFD0, transparent: true, opacity: 0.8 }); - this.elevationArc = new THREE.Mesh(new THREE.TubeGeometry(arcCurve, 16, 0.04, 6, false), elArcMat); - this.scene.add(this.elevationArc); - - const elHandleMat = new THREE.MeshBasicMaterial({ color: 0x00FFD0 }); - this.elevationHandle = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 12), elHandleMat); - this.scene.add(this.elevationHandle); - - const elGlowMat = new THREE.MeshBasicMaterial({ color: 0x00FFD0, transparent: true, opacity: 0.2 }); - this.elGlow = new THREE.Mesh(new THREE.SphereGeometry(0.22, 8, 8), elGlowMat); - this.scene.add(this.elGlow); - } - - createDistanceHandle() { - const distHandleMat = new THREE.MeshBasicMaterial({ color: 0xFFB800 }); - this.distanceHandle = new THREE.Mesh(new THREE.SphereGeometry(0.15, 12, 12), distHandleMat); - this.scene.add(this.distanceHandle); - - const distGlowMat = new THREE.MeshBasicMaterial({ color: 0xFFB800, transparent: true, opacity: 0.25 }); - this.distGlow = new THREE.Mesh(new THREE.SphereGeometry(0.22, 8, 8), distGlowMat); - this.scene.add(this.distGlow); - } - - updateDistanceLine(start, end) { - if (!this.distanceLine) { - const lineMat = new THREE.LineBasicMaterial({ color: 0xFFB800, transparent: true, opacity: 0.8 }); - this.distanceLine = new THREE.Line(new THREE.BufferGeometry(), lineMat); - this.scene.add(this.distanceLine); - } - const positions = new Float32Array([start.x, start.y, start.z, end.x, end.y, end.z]); - this.distanceLine.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); - this.distanceLine.geometry.attributes.position.needsUpdate = true; - } - - updateVisuals() { - const azRad = (this.liveAzimuth * Math.PI) / 180; - const elRad = (this.liveElevation * Math.PI) / 180; - const visualDist = 2.6 - (this.liveDistance / 10) * 2.0; - - const camX = visualDist * Math.sin(azRad) * Math.cos(elRad); - const camY = this.CENTER.y + visualDist * Math.sin(elRad); - const camZ = visualDist * Math.cos(azRad) * Math.cos(elRad); - - this.cameraIndicator.position.set(camX, camY, camZ); - this.cameraIndicator.lookAt(this.CENTER); - this.cameraIndicator.rotateX(Math.PI / 2); - this.camGlow.position.copy(this.cameraIndicator.position); - - const azX = this.AZIMUTH_RADIUS * Math.sin(azRad); - const azZ = this.AZIMUTH_RADIUS * Math.cos(azRad); - this.azimuthHandle.position.set(azX, 0.16, azZ); - this.azGlow.position.copy(this.azimuthHandle.position); - - const elY = this.CENTER.y + this.ELEVATION_RADIUS * Math.sin(elRad); - const elZ = this.ELEVATION_RADIUS * Math.cos(elRad); - this.elevationHandle.position.set(this.ELEV_ARC_X, elY, elZ); - this.elGlow.position.copy(this.elevationHandle.position); - - const distT = 0.15 + ((10 - this.liveDistance) / 10) * 0.7; - this.distanceHandle.position.lerpVectors(this.CENTER, this.cameraIndicator.position, distT); - this.distGlow.position.copy(this.distanceHandle.position); - - this.updateDistanceLine(this.CENTER.clone(), this.cameraIndicator.position.clone()); - this.requestRender(); - } - - bindEvents() { - const canvas = this.renderer.domElement; - - canvas.addEventListener('mousedown', (e) => this.onPointerDown(e)); - canvas.addEventListener('mousemove', (e) => this.onPointerMove(e)); - canvas.addEventListener('mouseup', () => this.onPointerUp()); - canvas.addEventListener('mouseleave', () => this.onPointerUp()); - - canvas.addEventListener('touchstart', (e) => { - e.preventDefault(); - this.onPointerDown({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }); - }, { passive: false }); - - canvas.addEventListener('touchmove', (e) => { - e.preventDefault(); - this.onPointerMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }); - }, { passive: false }); - - canvas.addEventListener('touchend', () => this.onPointerUp()); - canvas.addEventListener('wheel', (e) => this.onWheel(e), { passive: false }); - - new ResizeObserver(() => this.onResize()).observe(this.canvasContainer); - } - - getMousePos(event) { - const rect = this.renderer.domElement.getBoundingClientRect(); - this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - } - - setHandleScale(handle, glow, scale) { - handle.scale.setScalar(scale); - if (glow) glow.scale.setScalar(scale); - } - - onPointerDown(event) { - this.getMousePos(event); - this.raycaster.setFromCamera(this.mouse, this.camera); - - const handles = [ - { mesh: this.azimuthHandle, glow: this.azGlow, name: 'azimuth' }, - { mesh: this.elevationHandle, glow: this.elGlow, name: 'elevation' }, - { mesh: this.distanceHandle, glow: this.distGlow, name: 'distance' } - ]; - - for (const h of handles) { - if (this.raycaster.intersectObject(h.mesh).length > 0) { - this.isDragging = true; - this.dragTarget = h.name; - this.setHandleScale(h.mesh, h.glow, 1.3); - this.renderer.domElement.style.cursor = 'grabbing'; - return; - } - } - } - - onPointerMove(event) { - this.getMousePos(event); - this.raycaster.setFromCamera(this.mouse, this.camera); - - if (!this.isDragging) { - const handles = [ - { mesh: this.azimuthHandle, glow: this.azGlow, name: 'azimuth' }, - { mesh: this.elevationHandle, glow: this.elGlow, name: 'elevation' }, - { mesh: this.distanceHandle, glow: this.distGlow, name: 'distance' } - ]; - - let foundHover = null; - for (const h of handles) { - if (this.raycaster.intersectObject(h.mesh).length > 0) { - foundHover = h; - break; - } - } - - if (this.hoveredHandle && this.hoveredHandle !== foundHover) { - this.setHandleScale(this.hoveredHandle.mesh, this.hoveredHandle.glow, 1.0); - } - - if (foundHover) { - this.setHandleScale(foundHover.mesh, foundHover.glow, 1.15); - this.renderer.domElement.style.cursor = 'grab'; - this.hoveredHandle = foundHover; - } else { - this.renderer.domElement.style.cursor = 'default'; - this.hoveredHandle = null; - } - return; - } - - const plane = new THREE.Plane(); - const intersect = new THREE.Vector3(); - - if (this.dragTarget === 'azimuth') { - plane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 0)); - if (this.raycaster.ray.intersectPlane(plane, intersect)) { - let angle = Math.atan2(intersect.x, intersect.z) * (180 / Math.PI); - if (angle < 0) angle += 360; - this.liveAzimuth = Math.max(0, Math.min(360, angle)); - this.state.azimuth = Math.round(this.liveAzimuth); - this.updateDisplay(); - this.updateVisuals(); - this.notifyStateChange(); - } - } else if (this.dragTarget === 'elevation') { - const elevPlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -this.ELEV_ARC_X); - if (this.raycaster.ray.intersectPlane(elevPlane, intersect)) { - const relY = intersect.y - this.CENTER.y; - const relZ = intersect.z; - let angle = Math.atan2(relY, relZ) * (180 / Math.PI); - angle = Math.max(-30, Math.min(60, angle)); - this.liveElevation = angle; - this.state.elevation = Math.round(this.liveElevation); - this.updateDisplay(); - this.updateVisuals(); - this.notifyStateChange(); - } - } else if (this.dragTarget === 'distance') { - const newDist = 5 - this.mouse.y * 5; - this.liveDistance = Math.max(0, Math.min(10, newDist)); - this.state.distance = Math.round(this.liveDistance * 10) / 10; - this.updateDisplay(); - this.updateVisuals(); - this.notifyStateChange(); - } - } - - onPointerUp() { - if (this.isDragging) { - const handles = [ - { mesh: this.azimuthHandle, glow: this.azGlow }, - { mesh: this.elevationHandle, glow: this.elGlow }, - { mesh: this.distanceHandle, glow: this.distGlow } - ]; - handles.forEach(h => this.setHandleScale(h.mesh, h.glow, 1.0)); - } - this.isDragging = false; - this.dragTarget = null; - this.renderer.domElement.style.cursor = 'default'; - } - - onWheel(event) { - event.preventDefault(); - const sensitivity = 0.01; - let newDistance = this.liveDistance - event.deltaY * sensitivity; - newDistance = Math.max(0, Math.min(10, newDistance)); - this.liveDistance = newDistance; - this.state.distance = Math.round(this.liveDistance * 10) / 10; - this.updateVisuals(); - this.updateDisplay(); - this.notifyStateChange(); - } - - onResize() { - const w = this.canvasContainer.clientWidth; - const h = this.canvasContainer.clientHeight; - if (w === 0 || h === 0) return; - this.camera.aspect = w / h; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(w, h); - this.requestRender(); - } - - animate() { - this.animationId = requestAnimationFrame(() => this.animate()); - this.time += 1; - if (this.needsRender || this.time % 30 === 0) { - if (this.time % 30 === 0) { - this.glowRing.rotation.z += 0.1; - } - this.renderer.render(this.scene, this.camera); - this.needsRender = false; - } - } - - requestRender() { - this.needsRender = true; - } - - generatePrompt() { - const hAngle = this.state.azimuth % 360; - - let hDirection; - if (hAngle < 22.5 || hAngle >= 337.5) hDirection = "front view"; - else if (hAngle < 67.5) hDirection = "front-right quarter view"; - else if (hAngle < 112.5) hDirection = "right side view"; - else if (hAngle < 157.5) hDirection = "back-right quarter view"; - else if (hAngle < 202.5) hDirection = "back view"; - else if (hAngle < 247.5) hDirection = "back-left quarter view"; - else if (hAngle < 292.5) hDirection = "left side view"; - else hDirection = "front-left quarter view"; - - let vDirection; - if (this.state.elevation < -15) vDirection = "low-angle shot"; - else if (this.state.elevation < 15) vDirection = "eye-level shot"; - else if (this.state.elevation < 45) vDirection = "elevated shot"; - else vDirection = "high-angle shot"; - - let distance; - if (this.state.distance < 2) distance = "wide shot"; - else if (this.state.distance < 6) distance = "medium shot"; - else distance = "close-up"; - - return ` ${hDirection} ${vDirection} ${distance}`; - } - - updateDisplay() { - this.hValueEl.textContent = `${Math.round(this.state.azimuth)}°`; - this.vValueEl.textContent = `${Math.round(this.state.elevation)}°`; - this.zValueEl.textContent = this.state.distance.toFixed(1); - this.promptEl.textContent = this.generatePrompt(); - this.syncDropdowns(); - } - - syncDropdowns() { - this.azimuthSelect.value = this.findClosestOption(this.state.azimuth, this.AZIMUTH_OPTIONS, true).toString(); - this.elevationSelect.value = this.findClosestOption(this.state.elevation, this.ELEVATION_OPTIONS, false).toString(); - this.distanceSelect.value = this.findClosestDistanceOption(this.state.distance).toString(); - } - - findClosestOption(value, options, isAzimuth = false) { - let closest = options[0].value; - let minDiff = Math.abs(value - options[0].value); - - for (const opt of options) { - let diff = Math.abs(value - opt.value); - if (isAzimuth) { - diff = Math.min(diff, Math.abs(value - opt.value - 360), Math.abs(value - opt.value + 360)); - } - if (diff < minDiff) { - minDiff = diff; - closest = opt.value; - } - } - return closest; - } - - findClosestDistanceOption(distance) { - if (distance < 2) return 1; - if (distance < 6) return 4; - return 8; - } - - notifyStateChange() { - if (this.onStateChange) { - this.onStateChange({ ...this.state }); - } - } - - resetToDefaults() { - this.state.azimuth = 0; - this.state.elevation = 0; - this.state.distance = 5.0; - this.liveAzimuth = 0; - this.liveElevation = 0; - this.liveDistance = 5.0; - this.updateVisuals(); - this.updateDisplay(); - this.notifyStateChange(); - } - - async handleFileUpload(event) { - const file = event.target.files?.[0]; - if (!file) return; - - try { - const formData = new FormData(); - formData.append('image', file); - formData.append('overwrite', 'true'); - - const response = await api.fetchApi('/upload/image', { method: 'POST', body: formData }); - - if (response.ok) { - const result = await response.json(); - const filename = result.name; - const subfolder = result.subfolder || ''; - - this.state.uploadedImageFile = subfolder ? `${subfolder}/${filename}` : filename; - const imageUrl = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder)}&type=input`); - this.updateImage(imageUrl); - this.notifyStateChange(); - } - } catch (error) { - console.error('Error uploading image:', error); - } - - event.target.value = ''; - } - - setState(newState) { - if (newState.azimuth !== undefined) { - this.state.azimuth = newState.azimuth; - this.liveAzimuth = newState.azimuth; - } - if (newState.elevation !== undefined) { - this.state.elevation = newState.elevation; - this.liveElevation = newState.elevation; - } - if (newState.distance !== undefined) { - this.state.distance = newState.distance; - this.liveDistance = newState.distance; - } - if (newState.imageUrl !== undefined) { - this.state.imageUrl = newState.imageUrl; - this.updateImage(newState.imageUrl); - } - this.updateVisuals(); - this.updateDisplay(); - } - - getState() { - return { ...this.state }; - } - - getPrompt() { - return this.generatePrompt(); - } - - updateImage(url) { - if (url) { - const img = new Image(); - if (!url.startsWith('data:')) { - img.crossOrigin = 'anonymous'; - } - - img.onload = () => { - if (this.planeMat.map) { - this.planeMat.map.dispose(); - } - const tex = new THREE.Texture(img); - tex.colorSpace = THREE.SRGBColorSpace; - tex.needsUpdate = true; - this.planeMat.map = tex; - this.planeMat.color.set(0xffffff); - this.planeMat.needsUpdate = true; - - const ar = img.width / img.height; - const maxSize = 1.5; - let scaleX, scaleY; - if (ar > 1) { - scaleX = maxSize; - scaleY = maxSize / ar; - } else { - scaleY = maxSize; - scaleX = maxSize * ar; - } - this.imagePlane.scale.set(scaleX, scaleY, 1); - this.imageFrame.scale.set(scaleX, scaleY, 1); - this.requestRender(); - }; - - img.onerror = () => { - this.planeMat.map = null; - this.planeMat.color.set(0xE93D82); - this.planeMat.needsUpdate = true; - this.requestRender(); - }; - - img.src = url; - } else { - this.planeMat.map = null; - this.planeMat.color.set(0x3a3a4a); - this.planeMat.needsUpdate = true; - this.imagePlane.scale.set(1, 1, 1); - this.imageFrame.scale.set(1, 1, 1); - this.requestRender(); - } - } - - dispose() { - if (this.animationId !== null) { - try { window.cancelAnimationFrame(this.animationId); } catch (e) {} - this.animationId = null; - } - try { this.renderer.dispose(); } catch (e) {} - try { this.scene.clear(); } catch (e) {} - } -} - -// ============== ComfyUI Integration ============== -const widgetInstances = new Map(); - -function createCameraWidget(node) { - const container = document.createElement('div'); - container.id = `synvow-multiangle-widget-${node.id}`; - container.style.cssText = 'width:100%;height:100%;min-height:350px'; - - const getWidgetValue = (name, defaultValue) => { - const widget = node.widgets?.find(w => w.name === name); - return widget ? Number(widget.value) : defaultValue; - }; - - const initialState = { - azimuth: getWidgetValue('horizontal_angle', 0), - elevation: getWidgetValue('vertical_angle', 0), - distance: getWidgetValue('zoom', 5.0) - }; - - const widget = node.addDOMWidget('camera_preview', 'synvow-multiangle', container, { - getMinHeight: () => 370, - hideOnZoom: false, - serialize: false - }); - - const uploadedImageWidget = node.addWidget('hidden', 'uploaded_image', '', () => {}); - - setTimeout(() => { - const cameraWidget = new CameraWidget({ - node, - container, - initialState, - onStateChange: (state) => { - const hWidget = node.widgets?.find(w => w.name === 'horizontal_angle'); - const vWidget = node.widgets?.find(w => w.name === 'vertical_angle'); - const zWidget = node.widgets?.find(w => w.name === 'zoom'); - - if (hWidget) hWidget.value = state.azimuth; - if (vWidget) vWidget.value = state.elevation; - if (zWidget) zWidget.value = state.distance; - if (state.uploadedImageFile) uploadedImageWidget.value = state.uploadedImageFile; - - app.graph?.setDirtyCanvas(true, true); - } - }); - - widgetInstances.set(node.id, cameraWidget); - - const setupWidgetSync = (widgetName, cam) => { - const w = node.widgets?.find(widget => widget.name === widgetName); - if (w) { - const origCallback = w.callback; - w.callback = (value) => { - if (origCallback) origCallback.call(w, value); - if (widgetName === 'horizontal_angle') cam.setState({ azimuth: Number(value) }); - else if (widgetName === 'vertical_angle') cam.setState({ elevation: Number(value) }); - else if (widgetName === 'zoom') cam.setState({ distance: Number(value) }); - }; - } - }; - - setupWidgetSync('horizontal_angle', cameraWidget); - setupWidgetSync('vertical_angle', cameraWidget); - setupWidgetSync('zoom', cameraWidget); - }, 100); - - widget.onRemove = () => { - const cameraWidget = widgetInstances.get(node.id); - if (cameraWidget) { - cameraWidget.dispose(); - widgetInstances.delete(node.id); - } - }; - - return { widget }; -} - -app.registerExtension({ - name: 'ComfyUI.SynvowMultiangle', - - nodeCreated(node) { - if (node.constructor?.comfyClass !== 'SynvowMultiangleCameraNode') return; - - const [oldWidth, oldHeight] = node.size; - node.setSize([Math.max(oldWidth, 350), Math.max(oldHeight, 520)]); - createCameraWidget(node); - } -}); - -// Listen for node execution to update 3D preview -api.addEventListener('executed', (event) => { - const detail = event.detail; - if (!detail?.node || !detail?.output) return; - - const nodeId = parseInt(detail.node, 10); - const cameraWidget = widgetInstances.get(nodeId); - if (!cameraWidget) return; - - const previewImage = detail.output?.preview_image; - if (previewImage?.length > 0 && previewImage[0]) { - const p = previewImage[0]; - const url = api.apiURL( - `/view?filename=${encodeURIComponent(p.filename)}&subfolder=${encodeURIComponent(p.subfolder || '')}&type=${encodeURIComponent(p.type || 'temp')}&t=${Date.now()}` - ); - cameraWidget.updateImage(url); - } -}); - -export { CameraWidget };