diff --git a/.gitignore b/.gitignore index 4048114..6b5997a 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ checkpoints/ *.tflite *.mlmodel + +# 第三方参考源码(不提交到git) +reference-sources/ diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index e65f394..f459f57 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -50,6 +50,7 @@ import { RefreshCw, Scissors, Expand, + Move3D, } from "lucide-react"; import { Dialog, @@ -370,6 +371,18 @@ export function AdminSettingsClient() { yScale: "1.5", // Y轴放大倍数 }); + // 角度编辑API配置状态 + const [angleEditConfig, setAngleEditConfig] = useState({ + enabled: false, + falApiKey: "", + loraScale: 0.9, + loraUrl: "", + inferenceSteps: 28, + guidanceScale: 3.5, + maxImageSize: 10, + timeout: 120, + }); + // 加载抠图API配置 const { data: rmbgConfigData } = useSWR("/api/admin/settings?key=rmbg_api_config", fetcher); useEffect(() => { @@ -428,6 +441,33 @@ export function AdminSettingsClient() { } }, [extendConfigData]); + // 加载角度编辑API配置 + const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher); + useEffect(() => { + if (angleEditConfigData?.setting?.value) { + const config = angleEditConfigData.setting.value as { + enabled?: boolean; + falApiKey?: string; + loraScale?: number; + loraUrl?: string; + inferenceSteps?: number; + guidanceScale?: number; + maxImageSize?: number; + timeout?: number; + }; + setAngleEditConfig({ + enabled: config.enabled || false, + falApiKey: config.falApiKey || "", + loraScale: config.loraScale ?? 0.9, + loraUrl: config.loraUrl || "", + inferenceSteps: config.inferenceSteps ?? 28, + guidanceScale: config.guidanceScale ?? 3.5, + maxImageSize: config.maxImageSize ?? 10, + timeout: config.timeout ?? 120, + }); + } + }, [angleEditConfigData]); + // 导航链接状态 const [navLinkForm, setNavLinkForm] = useState({ id: "", @@ -828,6 +868,39 @@ export function AdminSettingsClient() { } }; + // --- 角度编辑API配置逻辑 --- + const handleSaveAngleEditConfig = async () => { + try { + if (angleEditConfig.enabled && !angleEditConfig.falApiKey.trim()) { + toast.error("启用角度编辑功能需要配置 fal.ai API Key"); + return; + } + + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: "angle_edit_config", + value: { + enabled: angleEditConfig.enabled, + falApiKey: angleEditConfig.falApiKey.trim(), + loraScale: angleEditConfig.loraScale, + loraUrl: angleEditConfig.loraUrl.trim() || undefined, + inferenceSteps: angleEditConfig.inferenceSteps, + guidanceScale: angleEditConfig.guidanceScale, + maxImageSize: angleEditConfig.maxImageSize, + timeout: angleEditConfig.timeout, + }, + }), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success("角度编辑API配置保存成功"); + } catch (error) { + toast.error("保存角度编辑API配置失败"); + } + }; + // --- 导航链接逻辑 --- const resetNavLinkForm = () => { setNavLinkForm({ id: "", name: "", url: "", order: 0, isEnabled: true }); @@ -3381,6 +3454,179 @@ export function AdminSettingsClient() { + + {/* 角度编辑API配置 */} + + + + + 角度编辑API配置 + + + 配置 fal.ai 多角度图像编辑 API,用于对同一图像生成不同视角的变体 + + + + {/* 启用开关 */} +
+
+ +
+ 开启后用户可在图片编辑器中使用角度编辑功能 +
+
+ setAngleEditConfig({ ...angleEditConfig, enabled: checked })} + /> +
+ + + + {/* API Key */} +
+ + setAngleEditConfig({ ...angleEditConfig, falApiKey: e.target.value })} + placeholder="fal_xxxxxxxxxxxxx" + disabled={!angleEditConfig.enabled} + /> +
+ 从{" "} + + fal.ai Dashboard + + {" "}获取 API Key +
+
+ + + + {/* LoRA 强度 */} +
+
+ + + {(angleEditConfig.loraScale * 100).toFixed(0)}% + +
+ setAngleEditConfig({ ...angleEditConfig, loraScale: Number.parseFloat(e.target.value) })} + min={0.5} + max={1.0} + step={0.05} + disabled={!angleEditConfig.enabled} + className="w-full" + /> +
+ 值越高角度变化越明显,值越低保留原图特征越多(推荐 85%-95%) +
+
+ + + + {/* 高级设置 */} +
+ + +
+ + setAngleEditConfig({ ...angleEditConfig, loraUrl: e.target.value })} + placeholder="留空使用默认地址" + disabled={!angleEditConfig.enabled} + /> +
+ 默认: huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA +
+
+ +
+
+ + setAngleEditConfig({ ...angleEditConfig, inferenceSteps: Number.parseInt(e.target.value) || 28 })} + min={10} + max={50} + disabled={!angleEditConfig.enabled} + /> +
+
+ + setAngleEditConfig({ ...angleEditConfig, guidanceScale: Number.parseFloat(e.target.value) || 3.5 })} + min={1} + max={10} + step={0.5} + disabled={!angleEditConfig.enabled} + /> +
+
+ +
+
+ + setAngleEditConfig({ ...angleEditConfig, maxImageSize: Number.parseInt(e.target.value) || 10 })} + min={1} + max={50} + disabled={!angleEditConfig.enabled} + /> +
+
+ + setAngleEditConfig({ ...angleEditConfig, timeout: Number.parseInt(e.target.value) || 120 })} + min={30} + max={300} + disabled={!angleEditConfig.enabled} + /> +
+
+
+ +
+
功能说明
+
+

• 使用 Qwen-Image-Edit-2511-Multiple-Angles-LoRA 模型

+

• 支持 96 种相机姿态(4 仰角 × 8 方位角 × 3 距离)

+

• 可生成正面、侧面、俯视、仰视等多角度图像

+

• 处理时间:约 10-30 秒/张

+

• 费用:约 $0.02-0.05/张(fal.ai 按量计费)

+
+
+ + +
+
diff --git a/app/api/angle-edit/route.ts b/app/api/angle-edit/route.ts new file mode 100644 index 0000000..4bfabeb --- /dev/null +++ b/app/api/angle-edit/route.ts @@ -0,0 +1,299 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSystemSetting } from "@/lib/db/queries"; +import { auth } from "@/app/(auth)/auth"; +import { uploadFile } from "@/lib/storage/service"; +import { db } from "@/lib/db"; +import { generatedImage } from "@/lib/db/schema"; + +// 角度编辑API配置类型 +type AngleEditConfig = { + enabled?: boolean; + falApiKey?: string; + loraScale?: number; + loraUrl?: string; + inferenceSteps?: number; + guidanceScale?: number; + maxImageSize?: number; + timeout?: number; +}; + +// 角度预设配置(使用官方提示词格式) +const ANGLE_PRESETS: Record = { + // 汽车常用角度 + "front": { azimuth: "front view", elevation: "eye-level shot", distance: "medium shot" }, + "front-3/4": { azimuth: "front-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, + "side": { azimuth: "right side view", elevation: "eye-level shot", distance: "medium shot" }, + "rear-3/4": { azimuth: "back-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, + "rear": { azimuth: "back view", elevation: "eye-level shot", distance: "medium shot" }, + "overhead": { azimuth: "front view", elevation: "high-angle shot", distance: "wide shot" }, + "low-front": { azimuth: "front view", elevation: "low-angle shot", distance: "close-up" }, + "low-side": { azimuth: "right side view", elevation: "low-angle shot", distance: "medium shot" }, +}; + +// 方位角映射(官方格式) +const AZIMUTH_MAP: Record = { + "front": "front view", + "front-right": "front-right quarter view", + "right": "right side view", + "back-right": "back-right quarter view", + "back": "back view", + "back-left": "back-left quarter view", + "left": "left side view", + "front-left": "front-left quarter view", +}; + +// 仰角映射(官方格式) +const ELEVATION_MAP: Record = { + "high": "high-angle shot", // 60° + "elevated": "elevated shot", // 30° + "eye-level": "eye-level shot", // 0° + "low": "low-angle shot", // -30° +}; + +// 距离映射(官方格式) +const DISTANCE_MAP: Record = { + "close": "close-up", // ×0.6 + "medium": "medium shot", // ×1.0 + "wide": "wide shot", // ×1.8 +}; + +// 默认 LoRA 地址 +const DEFAULT_LORA_URL = + "https://huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA/resolve/main/qwen-image-edit-2511-multiple-angles-lora.safetensors"; + +// 设置API路由最大执行时间为3分钟(180秒) +export const maxDuration = 180; + +/** + * 压缩图片生成缩略图 + */ +async function createThumbnail(buffer: Buffer): Promise { + try { + const sharp = await import("sharp"); + + const thumbnailBuffer = await sharp.default(buffer) + .resize(500, null, { + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: 80 }) + .toBuffer(); + + console.log(`[Angle-Edit] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`); + return thumbnailBuffer; + } catch (error) { + console.error("[Angle-Edit] Failed to create thumbnail:", error); + return buffer; + } +} + +/** + * 将 base64 图片上传到 S3 + */ +async function uploadBase64ToS3(base64Data: string, prefix: string = "angle-edit"): Promise<{ thumbnail: string; original: string }> { + try { + const buffer = Buffer.from(base64Data, "base64"); + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).slice(2, 8); + const thumbnailFilename = `${prefix}-thumb-${timestamp}-${randomStr}.png`; + const originalFilename = `${prefix}-orig-${timestamp}-${randomStr}.png`; + + console.log(`[Angle-Edit] Original size: ${buffer.length} bytes`); + + const thumbnailBuffer = await createThumbnail(buffer); + + const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice( + thumbnailBuffer.byteOffset, + thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength + ) as ArrayBuffer; + const originalArrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) as ArrayBuffer; + + console.log(`[Angle-Edit] Uploading thumbnail and original to S3...`); + const [thumbnailResult, originalResult] = await Promise.all([ + uploadFile(thumbnailFilename, thumbnailArrayBuffer), + uploadFile(originalFilename, originalArrayBuffer), + ]); + + console.log(`[Angle-Edit] Uploaded to S3:`); + console.log(` - Thumbnail: ${thumbnailResult.url}`); + console.log(` - Original: ${originalResult.url}`); + + return { + thumbnail: thumbnailResult.url, + original: originalResult.url, + }; + } catch (error) { + console.error("[Angle-Edit] Failed to upload to S3:", error); + throw new Error(`图片上传失败: ${error instanceof Error ? error.message : "未知错误"}`); + } +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + try { + // 1. 验证用户登录 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未登录" }, { status: 401 }); + } + + console.log("[Angle-Edit] ===== 收到请求 ====="); + console.log("[Angle-Edit] 用户ID:", session.user.id); + + // 2. 从数据库获取配置 + const angleEditConfig = await getSystemSetting({ key: "angle_edit_config" }); + const config = (angleEditConfig?.value as AngleEditConfig) || {}; + + // 验证功能是否启用 + if (!config.enabled) { + return NextResponse.json( + { error: "角度编辑功能未启用,请联系管理员在系统设置中开启" }, + { status: 400 } + ); + } + + // 验证 API Key + if (!config.falApiKey) { + return NextResponse.json( + { error: "fal.ai API Key 未配置,请联系管理员配置" }, + { status: 500 } + ); + } + + console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 0.9); + + // 3. 解析请求 + const body = await request.json(); + const { + image_url, + image_base64, + preset, + azimuth, + elevation, + distance, + lora_scale, + } = body; + + if (!image_url && !image_base64) { + return NextResponse.json({ error: "缺少图片数据" }, { status: 400 }); + } + + // 使用请求中的 lora_scale 或配置中的默认值 + const finalLoraScale = lora_scale ?? config.loraScale ?? 0.9; + + // 4. 构建角度参数 + let angleParams: { azimuth: string; elevation: string; distance: string }; + + if (preset && ANGLE_PRESETS[preset]) { + angleParams = ANGLE_PRESETS[preset]; + console.log("[Angle-Edit] 使用预设:", preset); + } else { + angleParams = { + azimuth: AZIMUTH_MAP[azimuth] || azimuth || "front view", + elevation: ELEVATION_MAP[elevation] || elevation || "eye-level shot", + distance: DISTANCE_MAP[distance] || distance || "medium shot", + }; + console.log("[Angle-Edit] 使用自定义参数:", angleParams); + } + + // 5. 构建提示词 + const prompt = ` ${angleParams.azimuth} ${angleParams.elevation} ${angleParams.distance}`; + console.log("[Angle-Edit] 提示词:", prompt); + + // 6. 准备图片输入 + let imageInput: string; + if (image_url) { + imageInput = image_url; + } else { + const mimeType = image_base64.startsWith("/9j/") ? "image/jpeg" : "image/png"; + imageInput = `data:${mimeType};base64,${image_base64}`; + } + + // 7. 动态导入 fal.ai SDK 并调用 API + console.log("[Angle-Edit] 调用 fal.ai API..."); + + const fal = await import("@fal-ai/serverless-client"); + + fal.config({ + credentials: config.falApiKey, + }); + + const loraUrl = config.loraUrl || DEFAULT_LORA_URL; + + const result = await fal.subscribe("fal-ai/qwen-image-edit", { + input: { + image_url: imageInput, + prompt: prompt, + lora_url: loraUrl, + lora_scale: finalLoraScale, + num_inference_steps: config.inferenceSteps || 28, + guidance_scale: config.guidanceScale || 3.5, + }, + logs: true, + onQueueUpdate: (update) => { + if (update.status === "IN_PROGRESS") { + console.log("[Angle-Edit] 处理进度..."); + } + }, + }); + + // 8. 处理结果 + const outputImage = (result as { images?: Array<{ url?: string }> }).images?.[0]; + if (!outputImage?.url) { + throw new Error("API 未返回有效图片"); + } + + const outputImageUrl = outputImage.url; + console.log("[Angle-Edit] 输出图片URL:", outputImageUrl); + + // 9. 下载图片并上传到我们的 S3 + const imageResponse = await fetch(outputImageUrl); + if (!imageResponse.ok) { + throw new Error(`下载输出图片失败: ${imageResponse.status}`); + } + const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); + const imageBase64Result = imageBuffer.toString("base64"); + + const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit"); + console.log("[Angle-Edit] 已上传到S3:", s3Result.original); + + // 10. 保存到数据库 + const processingTime = (Date.now() - startTime) / 1000; + + await db.insert(generatedImage).values({ + userId: session.user.id, + thumbnailUrl: s3Result.thumbnail, + originalUrl: s3Result.original, + prompt: `角度编辑: ${preset || `${azimuth}-${elevation}-${distance}`}`, + modelId: "qwen-image-edit-angle", + createdAt: new Date(), + }); + + console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒"); + + // 11. 返回结果 + return NextResponse.json({ + success: true, + image_url: s3Result.original, + thumbnail_url: s3Result.thumbnail, + image_base64: imageBase64Result, + processing_time: processingTime, + preset: preset, + angles: angleParams, + }); + + } catch (error) { + console.error("[Angle-Edit] ❌ 错误:", error); + return NextResponse.json( + { + error: error instanceof Error ? error.message : "角度编辑处理失败", + }, + { status: 500 } + ); + } +} + diff --git a/components/angle-controller-3d.tsx b/components/angle-controller-3d.tsx new file mode 100644 index 0000000..8b232c3 --- /dev/null +++ b/components/angle-controller-3d.tsx @@ -0,0 +1,499 @@ +"use client"; + +import { useRef, useState, useCallback, useMemo, Suspense, useEffect } from "react"; +import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import { OrbitControls, PerspectiveCamera, Html, useHelper } from "@react-three/drei"; +import type { OrbitControls as OrbitControlsImpl } from "three-stdlib"; +import * as THREE from "three"; +import { cn } from "@/lib/utils"; + +// 角度配置常量 +const AZIMUTH_OPTIONS = [ + { angle: 0, label: "正前方", value: "front" }, + { angle: 45, label: "前右", value: "front-right" }, + { angle: 90, label: "右侧", value: "right" }, + { angle: 135, label: "后右", value: "back-right" }, + { angle: 180, label: "正后方", value: "back" }, + { angle: 225, label: "后左", value: "back-left" }, + { angle: 270, label: "左侧", value: "left" }, + { angle: 315, label: "前左", value: "front-left" }, +]; + +const ELEVATION_OPTIONS = [ + { angle: -30, label: "仰视", value: "low" }, + { angle: 0, label: "平视", value: "eye-level" }, + { angle: 30, label: "俯视", value: "elevated" }, + { angle: 60, label: "高角度", value: "high" }, +]; + +const DISTANCE_OPTIONS = [ + { factor: 0.6, label: "特写", value: "close" }, + { factor: 1.0, label: "中景", value: "medium" }, + { factor: 1.8, label: "远景", value: "wide" }, +]; + +interface AngleController3DProps { + onChange?: (angles: { + azimuth: string; + elevation: string; + distance: string; + azimuthDegree: number; + elevationDegree: number; + distanceFactor: number; + }) => void; + initialAzimuth?: number; + initialElevation?: number; + initialDistance?: number; + showObject?: boolean; + className?: string; +} + +// 3D 场景中的相机指示器 - 简约现代风格 +function CameraIndicator({ position }: { position: THREE.Vector3 }) { + // 计算相机朝向中心的旋转 + const lookAtCenter = useMemo(() => { + const quaternion = new THREE.Quaternion(); + const up = new THREE.Vector3(0, 1, 0); + const matrix = new THREE.Matrix4(); + matrix.lookAt(position, new THREE.Vector3(0, 0, 0), up); + quaternion.setFromRotationMatrix(matrix); + return quaternion; + }, [position]); + + return ( + + {/* 相机主体 - 哑光黑/深灰 */} + + + + + + {/* 相机顶部 */} + + + + + + {/* 镜头筒 - 金属质感 */} + + + + + + {/* 镜头玻璃 - 蓝色反光 */} + + + + + + {/* 视野指示 - 极简线条 */} + + + + + + + + + {/* 连接线 */} + {[ + [-0.3, -0.2, 1.0], [0.3, -0.2, 1.0], [0.3, 0.2, 1.0], [-0.3, 0.2, 1.0] + ].map((point, i) => ( + + + + + + + ))} + + + ); +} + +// 方位角刻度圆环 - 更加隐形 +function AzimuthRing() { + return ( + + {/* 圆环 */} + + + + + + {/* 方位角标签 */} + {AZIMUTH_OPTIONS.map((opt) => { + const rad = (opt.angle * Math.PI) / 180; + const x = Math.sin(rad) * 2.3; + const z = Math.cos(rad) * 2.3; + return ( + +
+ {opt.label} +
+ + ); + })} +
+ ); +} + +// 目标物体 - 极简风格 +function TargetObject() { + return ( + + {/* 极简几何体代表目标 */} + + + + + + {/* 底部阴影 */} + + + + + + {/* 网格地面 */} + + + ); +} + +// 3D 控制场景 +function Scene({ + azimuth, + elevation, + distance, + onAngleChange, + showObject, +}: { + azimuth: number; + elevation: number; + distance: number; + onAngleChange: (azimuth: number, elevation: number) => void; + showObject: boolean; +}) { + const controlsRef = useRef(null); + const ghostCameraRef = useRef(null); + const mainCameraRef = useRef(null); + + // 计算目标拍摄位置(CameraIndicator的位置) + const targetPosition = useMemo(() => { + const azimuthRad = (azimuth * Math.PI) / 180; + const elevationRad = (elevation * Math.PI) / 180; + const radius = 2 * distance; + + const x = radius * Math.cos(elevationRad) * Math.sin(azimuthRad); + const y = radius * Math.sin(elevationRad); + const z = radius * Math.cos(elevationRad) * Math.cos(azimuthRad); + + return new THREE.Vector3(x, y, z); + }, [azimuth, elevation, distance]); + + // 同步 Ghost Camera 位置到 state + // 当 state 改变时,更新 Ghost Camera 位置 + useEffect(() => { + if (ghostCameraRef.current) { + ghostCameraRef.current.position.copy(targetPosition); + ghostCameraRef.current.lookAt(0, 0, 0); + } + }, [targetPosition]); + + // 监听控制器变化 (用户拖拽) + const handleControlsChange = useCallback(() => { + if (ghostCameraRef.current) { + const camera = ghostCameraRef.current; + const spherical = new THREE.Spherical(); + spherical.setFromVector3(camera.position); + + // 将球面坐标转换为我们的角度系统 + let newAzimuth = THREE.MathUtils.radToDeg(spherical.theta); + if (newAzimuth < 0) newAzimuth += 360; + + const newElevation = 90 - THREE.MathUtils.radToDeg(spherical.phi); + + // 只有当角度变化超过阈值时才触发更新,避免浮点数抖动 + if (Math.abs(newAzimuth - azimuth) > 0.5 || Math.abs(newElevation - elevation) > 0.5) { + onAngleChange(Math.round(newAzimuth), Math.round(newElevation)); + } + } + }, [onAngleChange, azimuth, elevation]); + + return ( + <> + {/* 主相机:上帝视角,固定位置 */} + c.lookAt(0, 0, 0)} + /> + + {/* 幽灵相机:用于 OrbitControls 交互,不可见 */} + + + {/* 控制器绑定到幽灵相机 */} + + + {/* 灯光 */} + + + + + {/* 场景内容 */} + {showObject && } + + + {/* 拍摄相机模型(在目标位置渲染) */} + + + {/* 坐标轴辅助 */} + + + ); +} + +// 主组件 +export function AngleController3D({ + onChange, + initialAzimuth = 45, + initialElevation = 0, + initialDistance = 1.0, + showObject = true, + className, +}: AngleController3DProps) { + const [azimuth, setAzimuth] = useState(initialAzimuth); + const [elevation, setElevation] = useState(initialElevation); + const [distance, setDistance] = useState(initialDistance); + + // 将角度映射到最近的预设值 + const getClosestAzimuth = useCallback((deg: number) => { + let minDiff = 360; + let closest = AZIMUTH_OPTIONS[0]; + for (const opt of AZIMUTH_OPTIONS) { + const diff = Math.min(Math.abs(deg - opt.angle), 360 - Math.abs(deg - opt.angle)); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + const getClosestElevation = useCallback((deg: number) => { + let minDiff = 180; + let closest = ELEVATION_OPTIONS[0]; + for (const opt of ELEVATION_OPTIONS) { + const diff = Math.abs(deg - opt.angle); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + const getClosestDistance = useCallback((factor: number) => { + let minDiff = 10; + let closest = DISTANCE_OPTIONS[0]; + for (const opt of DISTANCE_OPTIONS) { + const diff = Math.abs(factor - opt.factor); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + // 处理角度变化 + const handleAngleChange = useCallback( + (newAzimuth: number, newElevation: number) => { + setAzimuth(newAzimuth); + setElevation(newElevation); + + const closestAzimuth = getClosestAzimuth(newAzimuth); + const closestElevation = getClosestElevation(newElevation); + const closestDistance = getClosestDistance(distance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: newAzimuth, + elevationDegree: newElevation, + distanceFactor: distance, + }); + }, + [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + // 处理距离变化 + const handleDistanceChange = useCallback( + (newDistance: number) => { + setDistance(newDistance); + + const closestAzimuth = getClosestAzimuth(azimuth); + const closestElevation = getClosestElevation(elevation); + const closestDistance = getClosestDistance(newDistance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: azimuth, + elevationDegree: elevation, + distanceFactor: newDistance, + }); + }, + [azimuth, elevation, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + // 快速选择预设角度 + const handlePresetClick = useCallback( + (presetAzimuth: number, presetElevation: number) => { + setAzimuth(presetAzimuth); + setElevation(presetElevation); + + const closestAzimuth = getClosestAzimuth(presetAzimuth); + const closestElevation = getClosestElevation(presetElevation); + const closestDistance = getClosestDistance(distance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: presetAzimuth, + elevationDegree: presetElevation, + distanceFactor: distance, + }); + }, + [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + const currentAzimuth = getClosestAzimuth(azimuth); + const currentElevation = getClosestElevation(elevation); + const currentDistance = getClosestDistance(distance); + + return ( +
+ {/* 3D 视图 */} +
+ + + + + + + {/* 当前角度显示 - 悬浮卡片风格 */} +
+ 方位: {currentAzimuth.label} ({azimuth}°) + 仰角: {currentElevation.label} ({elevation}°) +
+ + {/* 操作提示 */} +
+ 🖱️ 拖拽旋转相机位置 +
+
+ + {/* 快速预设按钮 */} +
+
快速选择
+
+ {[ + { label: "正前", azimuth: 0, elevation: 0 }, + { label: "前3/4", azimuth: 45, elevation: 0 }, + { label: "侧面", azimuth: 90, elevation: 0 }, + { label: "后3/4", azimuth: 135, elevation: 0 }, + { label: "正后", azimuth: 180, elevation: 0 }, + { label: "俯视", azimuth: 0, elevation: 60 }, + { label: "仰视", azimuth: 0, elevation: -30 }, + { label: "鸟瞰", azimuth: 45, elevation: 60 }, + ].map((preset) => ( + + ))} +
+
+ + {/* 距离控制 */} +
+
镜头距离
+
+ {DISTANCE_OPTIONS.map((opt) => ( + + ))} +
+
+
+ ); +} + +export default AngleController3D; diff --git a/components/angle-edit-dialog.tsx b/components/angle-edit-dialog.tsx new file mode 100644 index 0000000..91cde91 --- /dev/null +++ b/components/angle-edit-dialog.tsx @@ -0,0 +1,312 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import { Label } from "@/components/ui/label"; +import { Loader2, Download, Check, RotateCcw, Move3D, Sparkles } from "lucide-react"; +import { toast } from "sonner"; +import { AngleController3D } from "./angle-controller-3d"; +import { cn } from "@/lib/utils"; + +interface AngleEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + imageUrl: string; + onComplete?: (resultUrl: string) => void; +} + +interface AngleParams { + azimuth: string; + elevation: string; + distance: string; + azimuthDegree: number; + elevationDegree: number; + distanceFactor: number; +} + +export function AngleEditDialog({ + open, + onOpenChange, + imageUrl, + onComplete, +}: AngleEditDialogProps) { + const [isProcessing, setIsProcessing] = useState(false); + const [resultUrl, setResultUrl] = useState(null); + const [loraScale, setLoraScale] = useState(0.9); + const [currentAngles, setCurrentAngles] = useState({ + azimuth: "front-right", + elevation: "eye-level", + distance: "medium", + azimuthDegree: 45, + elevationDegree: 0, + distanceFactor: 1.0, + }); + + // 处理角度变化 + const handleAngleChange = useCallback((angles: AngleParams) => { + setCurrentAngles(angles); + // 清除之前的结果,因为角度变了 + setResultUrl(null); + }, []); + + // 生成新视角 + const handleGenerate = useCallback(async () => { + setIsProcessing(true); + setResultUrl(null); + + try { + const response = await fetch("/api/angle-edit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + image_url: imageUrl, + azimuth: currentAngles.azimuth, + elevation: currentAngles.elevation, + distance: currentAngles.distance, + lora_scale: loraScale, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "生成失败"); + } + + const data = await response.json(); + setResultUrl(data.image_url); + toast.success("视角生成成功!"); + } catch (error) { + console.error("角度编辑失败:", error); + toast.error(error instanceof Error ? error.message : "生成失败,请重试"); + } finally { + setIsProcessing(false); + } + }, [imageUrl, currentAngles, loraScale]); + + // 下载结果图片 + const handleDownload = useCallback(async () => { + if (!resultUrl) return; + + try { + const response = await fetch(resultUrl); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `angle-edit-${currentAngles.azimuth}-${currentAngles.elevation}-${Date.now()}.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success("图片已下载"); + } catch { + toast.error("下载失败"); + } + }, [resultUrl, currentAngles]); + + // 使用此图片 + const handleUseImage = useCallback(() => { + if (resultUrl) { + onComplete?.(resultUrl); + onOpenChange(false); + } + }, [resultUrl, onComplete, onOpenChange]); + + // 重置 + const handleReset = useCallback(() => { + setResultUrl(null); + setCurrentAngles({ + azimuth: "front-right", + elevation: "eye-level", + distance: "medium", + azimuthDegree: 45, + elevationDegree: 0, + distanceFactor: 1.0, + }); + setLoraScale(0.9); + }, []); + + return ( + + + +
+
+
+ +
+
+ 多角度编辑 + + 拖拽 3D 控制器选择相机角度,AI 将生成对应视角的图像 + +
+
+
+
+ +
+
+ {/* 左侧:3D 控制器 (占 5 列) */} +
+
+ +
+ + {/* LoRA 强度控制 - 放入卡片 */} +
+
+ + + {(loraScale * 100).toFixed(0)}% + +
+ setLoraScale(v)} + min={0.5} + max={1.0} + step={0.05} + className="py-2" + /> +

+ 强度越高角度变化越明显,越低则保留更多原图特征。建议 85-95%。 +

+
+
+ + {/* 右侧:图片预览 (占 7 列) */} +
+
+ {/* 原图 */} +
+
+ 原图 +
+ 原图 +
+ + {/* 结果图 */} +
+
+
+ 生成结果 +
+ {resultUrl && ( + + 完成 + + )} +
+ +
+ {isProcessing ? ( +
+
+
+
+ +
+
+
+

正在生成新视角...

+

预计需要 10-30 秒

+
+
+ ) : resultUrl ? ( + 生成结果 + ) : ( +
+
+ +
+

准备就绪

+

调整左侧角度后点击生成

+
+ )} +
+
+
+
+
+
+ + {/* 底部操作栏 */} +
+ + +
+ {resultUrl ? ( + <> + + + + ) : ( + + )} +
+
+ +
+ ); +} + +export default AngleEditDialog; + diff --git a/components/image-preview.tsx b/components/image-preview.tsx index 84df4f4..01072d7 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -1,9 +1,10 @@ "use client"; import React, { useState } from "react"; -import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2 } from "lucide-react"; +import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; +import { AngleEditDialog } from "./angle-edit-dialog"; export function ImagePreview({ src, @@ -14,9 +15,11 @@ export function ImagePreview({ onImageEnhance, onImageRmbg, onImageExtend, + onImageAngleEdit, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, + isProcessingAngleEdit, imageIndex }: { src: string; @@ -31,9 +34,11 @@ export function ImagePreview({ onImageEnhance?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; + onImageAngleEdit?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; + isProcessingAngleEdit?: boolean; imageIndex?: number; }) { const [isOpen, setIsOpen] = useState(false); @@ -41,6 +46,7 @@ export function ImagePreview({ const [isDetecting, setIsDetecting] = useState(false); const [showAnnotator, setShowAnnotator] = useState(false); const [showResizeEditor, setShowResizeEditor] = useState(false); + const [showAngleEditDialog, setShowAngleEditDialog] = useState(false); // 解析 alt 文本,提取原图 URL 和宽度 // 格式优先级: @@ -272,11 +278,11 @@ export function ImagePreview({ className="flex items-center gap-1.5 rounded-full bg-black/70 hover:bg-black/80 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed" onClick={(e) => { e.stopPropagation(); - if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) { + if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit) { onImageExtend(original); } }} - disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend} + disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit} type="button" aria-label="图像拓展" title="图像拓展" @@ -285,6 +291,24 @@ export function ImagePreview({ 图像拓展 )} + {onImageAngleEdit && ( + + )} + + + {/* LoRA 模型地址 */} +
+ + setAngleEditLoraUrl(e.target.value)} + placeholder="留空使用默认地址" + disabled={!angleEditEnabled} + /> +

+ 默认: huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA +

+
+ + {/* 推理参数 */} +
+
+ + setAngleEditInferenceSteps(Number(e.target.value))} + min={10} + max={50} + disabled={!angleEditEnabled} + /> +
+
+ + setAngleEditGuidanceScale(Number(e.target.value))} + min={1} + max={10} + step={0.5} + disabled={!angleEditEnabled} + /> +
+
+ + setAngleEditMaxImageSize(Number(e.target.value))} + min={1} + max={50} + disabled={!angleEditEnabled} + /> +
+
+ + {/* 超时设置 */} +
+ + setAngleEditTimeout(Number(e.target.value))} + min={30} + max={300} + disabled={!angleEditEnabled} + className="w-32" + /> +

+ API 调用超时时间,建议 60-180 秒 +

+
+
+ + + {/* 保存按钮 */} + + + +``` + +### 3.4 后端配置读取 + +**文件:`app/api/angle-edit/route.ts`** + +从数据库读取配置: + +```typescript +// 获取配置 +const [configRow] = await db + .select() + .from(systemConfig) + .where(eq(systemConfig.key, "angleEditConfig")); + +const config = (configRow?.value as AngleEditConfig) || {}; + +// 验证配置 +if (!config.enabled) { + return NextResponse.json( + { error: "角度编辑功能未启用,请联系管理员在系统设置中开启" }, + { status: 400 } + ); +} + +if (!config.falApiKey) { + return NextResponse.json( + { error: "fal.ai API Key 未配置,请联系管理员配置" }, + { status: 500 } + ); +} + +// 使用配置 +const loraUrl = config.loraUrl || + "https://huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA/resolve/main/qwen-image-edit-2511-multiple-angles-lora.safetensors"; + +const result = await fal.subscribe("fal-ai/qwen-image-edit", { + input: { + image_url: imageInput, + prompt: prompt, + lora_url: loraUrl, + lora_scale: config.loraScale || 0.9, + num_inference_steps: config.inferenceSteps || 28, + guidance_scale: config.guidanceScale || 3.5, + }, + // ... +}); +``` + +--- + +## 📦 四、完整实现代码 + +### 4.1 安装依赖 + +```bash +# 安装 fal.ai SDK +npm install @fal-ai/serverless-client + +# 安装 Three.js 和 React Three Fiber(用于 3D 控制器) +npm install @react-three/fiber @react-three/drei three +npm install -D @types/three +``` + +### 4.2 后端 API 路由 + +**文件:`app/api/angle-edit/route.ts`** + +```typescript +import { NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { db } from "@/lib/db"; +import { systemConfig, userImages } from "@/lib/db/schema"; +import { eq } from "drizzle-orm"; +import * as fal from "@fal-ai/serverless-client"; + +// 配置类型定义 +interface AngleEditConfig { + enabled: boolean; + falApiKey: string; + loraScale: number; + loraUrl?: string; + inferenceSteps: number; + guidanceScale: number; + maxImageSize: number; + timeout: number; +} + +// 角度预设配置(使用官方提示词格式) +const ANGLE_PRESETS: Record = { + // 汽车常用角度 + "front": { azimuth: "front view", elevation: "eye-level shot", distance: "medium shot" }, + "front-3/4": { azimuth: "front-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, + "side": { azimuth: "right side view", elevation: "eye-level shot", distance: "medium shot" }, + "rear-3/4": { azimuth: "back-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, + "rear": { azimuth: "back view", elevation: "eye-level shot", distance: "medium shot" }, + "overhead": { azimuth: "front view", elevation: "high-angle shot", distance: "wide shot" }, + "low-front": { azimuth: "front view", elevation: "low-angle shot", distance: "close-up" }, + "low-side": { azimuth: "right side view", elevation: "low-angle shot", distance: "medium shot" }, +}; + +// 方位角映射(官方格式) +const AZIMUTH_MAP: Record = { + "front": "front view", + "front-right": "front-right quarter view", + "right": "right side view", + "back-right": "back-right quarter view", + "back": "back view", + "back-left": "back-left quarter view", + "left": "left side view", + "front-left": "front-left quarter view", +}; + +// 仰角映射(官方格式) +const ELEVATION_MAP: Record = { + "high": "high-angle shot", // 60° + "elevated": "elevated shot", // 30° + "eye-level": "eye-level shot", // 0° + "low": "low-angle shot", // -30° +}; + +// 距离映射(官方格式) +const DISTANCE_MAP: Record = { + "close": "close-up", // ×0.6 + "medium": "medium shot", // ×1.0 + "wide": "wide shot", // ×1.8 +}; + +// 默认 LoRA 地址 +const DEFAULT_LORA_URL = + "https://huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA/resolve/main/qwen-image-edit-2511-multiple-angles-lora.safetensors"; + +export async function POST(request: Request) { + const startTime = Date.now(); + + try { + // 1. 验证用户登录 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未登录" }, { status: 401 }); + } + + console.log("[Angle-Edit] ===== 收到请求 ====="); + console.log("[Angle-Edit] 用户ID:", session.user.id); + + // 2. 从数据库获取配置 + const [configRow] = await db + .select() + .from(systemConfig) + .where(eq(systemConfig.key, "angleEditConfig")); + + const config = (configRow?.value as AngleEditConfig) || {}; + + // 验证功能是否启用 + if (!config.enabled) { + return NextResponse.json( + { error: "角度编辑功能未启用,请联系管理员在系统设置中开启" }, + { status: 400 } + ); + } + + // 验证 API Key + if (!config.falApiKey) { + return NextResponse.json( + { error: "fal.ai API Key 未配置,请联系管理员配置" }, + { status: 500 } + ); + } + + console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale); + + // 配置 fal.ai + fal.config({ + credentials: config.falApiKey, + }); + + // 3. 解析请求 + const body = await request.json(); + const { + image_url, + image_base64, + preset, + azimuth, + elevation, + distance, + lora_scale, + } = body; + + if (!image_url && !image_base64) { + return NextResponse.json({ error: "缺少图片数据" }, { status: 400 }); + } + + // 使用请求中的 lora_scale 或配置中的默认值 + const finalLoraScale = lora_scale ?? config.loraScale ?? 0.9; + + // 4. 构建角度参数 + let angleParams: { azimuth: string; elevation: string; distance: string }; + + if (preset && ANGLE_PRESETS[preset]) { + angleParams = ANGLE_PRESETS[preset]; + console.log("[Angle-Edit] 使用预设:", preset); + } else { + angleParams = { + azimuth: AZIMUTH_MAP[azimuth] || azimuth || "front view", + elevation: ELEVATION_MAP[elevation] || elevation || "eye-level shot", + distance: DISTANCE_MAP[distance] || distance || "medium shot", + }; + console.log("[Angle-Edit] 使用自定义参数:", angleParams); + } + + // 5. 构建提示词 + const prompt = ` ${angleParams.azimuth} ${angleParams.elevation} ${angleParams.distance}`; + console.log("[Angle-Edit] 提示词:", prompt); + + // 6. 准备图片输入 + let imageInput: string; + if (image_url) { + imageInput = image_url; + } else { + const mimeType = image_base64.startsWith("/9j/") ? "image/jpeg" : "image/png"; + imageInput = `data:${mimeType};base64,${image_base64}`; + } + + // 7. 调用 fal.ai API + console.log("[Angle-Edit] 调用 fal.ai API..."); + + const loraUrl = config.loraUrl || DEFAULT_LORA_URL; + + const result = await fal.subscribe("fal-ai/qwen-image-edit", { + input: { + image_url: imageInput, + prompt: prompt, + lora_url: loraUrl, + lora_scale: finalLoraScale, + num_inference_steps: config.inferenceSteps || 28, + guidance_scale: config.guidanceScale || 3.5, + }, + logs: true, + onQueueUpdate: (update) => { + if (update.status === "IN_PROGRESS") { + console.log("[Angle-Edit] 处理进度..."); + } + }, + }); + + // 8. 处理结果 + const outputImage = (result as any).images?.[0]; + if (!outputImage?.url) { + throw new Error("API 未返回有效图片"); + } + + const outputImageUrl = outputImage.url; + console.log("[Angle-Edit] 输出图片URL:", outputImageUrl); + + // 9. 下载图片并上传到我们的 S3 + const imageResponse = await fetch(outputImageUrl); + if (!imageResponse.ok) { + throw new Error(`下载输出图片失败: ${imageResponse.status}`); + } + const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); + const imageBase64Result = imageBuffer.toString("base64"); + + const { uploadToS3 } = await import("@/lib/s3"); + + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).substring(2, 8); + const fileName = `angle-edit/angle-${preset || "custom"}-${timestamp}-${randomStr}.png`; + + const s3Url = await uploadToS3(imageBuffer, fileName, "image/png"); + console.log("[Angle-Edit] 已上传到S3:", s3Url); + + // 10. 保存到数据库 + const processingTime = (Date.now() - startTime) / 1000; + + await db.insert(userImages).values({ + userId: session.user.id, + originalUrl: image_url || "", + thumbnailUrl: s3Url, + originalImageUrl: s3Url, + processType: "angle-edit", + prompt: `角度编辑: ${preset || `${azimuth}-${elevation}-${distance}`}`, + processingTime: processingTime, + createdAt: new Date(), + }); + + console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒"); + + // 11. 返回结果 + return NextResponse.json({ + success: true, + image_url: s3Url, + image_base64: imageBase64Result, + processing_time: processingTime, + preset: preset, + angles: angleParams, + }); + + } catch (error) { + console.error("[Angle-Edit] ❌ 错误:", error); + return NextResponse.json( + { + error: error instanceof Error ? error.message : "角度编辑处理失败", + }, + { status: 500 } + ); + } +} +``` + +### 4.3 安装 3D 相关依赖 + +```bash +# 安装 React Three Fiber 和相关库 +npm install @react-three/fiber @react-three/drei three +npm install -D @types/three +``` + +### 4.4 3D 角度控制器组件 + +**文件:`components/angle-controller-3d.tsx`** + +这是一个基于 Three.js 的交互式 3D 控制器,用户可以拖拽旋转来选择相机角度: + +```tsx +"use client"; + +import { useRef, useState, useCallback, useMemo, Suspense, useEffect } from "react"; +import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import { OrbitControls, PerspectiveCamera, Html } from "@react-three/drei"; +import type { OrbitControls as OrbitControlsImpl } from "three-stdlib"; +import * as THREE from "three"; +import { cn } from "@/lib/utils"; + +// 角度配置常量 +const AZIMUTH_OPTIONS = [ + { angle: 0, label: "正前方", value: "front" }, + { angle: 45, label: "前右", value: "front-right" }, + { angle: 90, label: "右侧", value: "right" }, + { angle: 135, label: "后右", value: "back-right" }, + { angle: 180, label: "正后方", value: "back" }, + { angle: 225, label: "后左", value: "back-left" }, + { angle: 270, label: "左侧", value: "left" }, + { angle: 315, label: "前左", value: "front-left" }, +]; + +const ELEVATION_OPTIONS = [ + { angle: -30, label: "仰视", value: "low" }, + { angle: 0, label: "平视", value: "eye-level" }, + { angle: 30, label: "俯视", value: "elevated" }, + { angle: 60, label: "高角度", value: "high" }, +]; + +const DISTANCE_OPTIONS = [ + { factor: 0.6, label: "特写", value: "close" }, + { factor: 1.0, label: "中景", value: "medium" }, + { factor: 1.8, label: "远景", value: "wide" }, +]; + +interface AngleController3DProps { + onChange?: (angles: { + azimuth: string; + elevation: string; + distance: string; + azimuthDegree: number; + elevationDegree: number; + distanceFactor: number; + }) => void; + initialAzimuth?: number; + initialElevation?: number; + initialDistance?: number; + showObject?: boolean; + className?: string; +} + +// 3D 场景中的相机指示器 - 更逼真的相机模型 +function CameraIndicator({ position }: { position: THREE.Vector3 }) { + // 计算相机朝向中心的旋转 + const lookAtCenter = useMemo(() => { + const direction = new THREE.Vector3(0, 0, 0).sub(position).normalize(); + const quaternion = new THREE.Quaternion(); + const up = new THREE.Vector3(0, 1, 0); + const matrix = new THREE.Matrix4(); + matrix.lookAt(position, new THREE.Vector3(0, 0, 0), up); + quaternion.setFromRotationMatrix(matrix); + return quaternion; + }, [position]); + + return ( + + {/* 相机主体 */} + + + + + + {/* 相机顶部(取景器凸起) */} + + + + + + {/* 镜头筒 */} + + + + + + {/* 镜头玻璃 */} + + + + + + {/* 镜头光圈环 */} + + + + + + {/* 相机手柄/握把 */} + + + + + + {/* 视野锥形指示线 */} + + + + + + + + {/* 视野框 */} + + + + + + + + ); +} + +// 方位角刻度圆环 +function AzimuthRing() { + const points: THREE.Vector3[] = []; + for (let i = 0; i <= 360; i += 5) { + const rad = (i * Math.PI) / 180; + points.push(new THREE.Vector3(Math.sin(rad) * 2, 0, Math.cos(rad) * 2)); + } + + return ( + + {/* 圆环 */} + + + [p.x, p.y, p.z]))} + itemSize={3} + /> + + + + + {/* 方位角标签 */} + {AZIMUTH_OPTIONS.map((opt) => { + const rad = (opt.angle * Math.PI) / 180; + const x = Math.sin(rad) * 2.3; + const z = Math.cos(rad) * 2.3; + return ( + +
+ {opt.label} +
+ + ); + })} +
+ ); +} + +// 目标物体占位符 +function TargetObject() { + return ( + + {/* 简化的汽车形状作为占位符 */} + + + + + + + + + {/* 地面参考 */} + + + + + + ); +} + +// 3D 控制场景 +function Scene({ + azimuth, + elevation, + distance, + onAngleChange, + showObject, +}: { + azimuth: number; + elevation: number; + distance: number; + onAngleChange: (azimuth: number, elevation: number) => void; + showObject: boolean; +}) { + const controlsRef = useRef(null); + const { camera } = useThree(); + + // 计算相机位置 + const cameraPosition = useMemo(() => { + const azimuthRad = (azimuth * Math.PI) / 180; + const elevationRad = (elevation * Math.PI) / 180; + const radius = 2 * distance; + + const x = radius * Math.cos(elevationRad) * Math.sin(azimuthRad); + const y = radius * Math.sin(elevationRad); + const z = radius * Math.cos(elevationRad) * Math.cos(azimuthRad); + + return new THREE.Vector3(x, y, z); + }, [azimuth, elevation, distance]); + + // 监听控制器变化 + useFrame(() => { + if (controlsRef.current) { + const spherical = new THREE.Spherical(); + spherical.setFromVector3(camera.position); + + // 将球面坐标转换为我们的角度系统 + let newAzimuth = THREE.MathUtils.radToDeg(spherical.theta); + if (newAzimuth < 0) newAzimuth += 360; + + const newElevation = 90 - THREE.MathUtils.radToDeg(spherical.phi); + + // 只在拖拽时更新角度 + if (controlsRef.current.enabled) { + onAngleChange(Math.round(newAzimuth), Math.round(newElevation)); + } + } + }); + + return ( + <> + + + + + + + + {showObject && } + + + + {/* 坐标轴辅助 */} + + + ); +} + +// 主组件 +export function AngleController3D({ + onChange, + initialAzimuth = 45, + initialElevation = 0, + initialDistance = 1.0, + showObject = true, + className, +}: AngleController3DProps) { + const [azimuth, setAzimuth] = useState(initialAzimuth); + const [elevation, setElevation] = useState(initialElevation); + const [distance, setDistance] = useState(initialDistance); + + // 将角度映射到最近的预设值 + const getClosestAzimuth = useCallback((deg: number) => { + let minDiff = 360; + let closest = AZIMUTH_OPTIONS[0]; + for (const opt of AZIMUTH_OPTIONS) { + const diff = Math.min(Math.abs(deg - opt.angle), 360 - Math.abs(deg - opt.angle)); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + const getClosestElevation = useCallback((deg: number) => { + let minDiff = 180; + let closest = ELEVATION_OPTIONS[0]; + for (const opt of ELEVATION_OPTIONS) { + const diff = Math.abs(deg - opt.angle); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + const getClosestDistance = useCallback((factor: number) => { + let minDiff = 10; + let closest = DISTANCE_OPTIONS[0]; + for (const opt of DISTANCE_OPTIONS) { + const diff = Math.abs(factor - opt.factor); + if (diff < minDiff) { + minDiff = diff; + closest = opt; + } + } + return closest; + }, []); + + // 处理角度变化 + const handleAngleChange = useCallback( + (newAzimuth: number, newElevation: number) => { + setAzimuth(newAzimuth); + setElevation(newElevation); + + const closestAzimuth = getClosestAzimuth(newAzimuth); + const closestElevation = getClosestElevation(newElevation); + const closestDistance = getClosestDistance(distance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: newAzimuth, + elevationDegree: newElevation, + distanceFactor: distance, + }); + }, + [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + // 处理距离变化 + const handleDistanceChange = useCallback( + (newDistance: number) => { + setDistance(newDistance); + + const closestAzimuth = getClosestAzimuth(azimuth); + const closestElevation = getClosestElevation(elevation); + const closestDistance = getClosestDistance(newDistance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: azimuth, + elevationDegree: elevation, + distanceFactor: newDistance, + }); + }, + [azimuth, elevation, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + // 快速选择预设角度 + const handlePresetClick = useCallback( + (presetAzimuth: number, presetElevation: number) => { + setAzimuth(presetAzimuth); + setElevation(presetElevation); + + const closestAzimuth = getClosestAzimuth(presetAzimuth); + const closestElevation = getClosestElevation(presetElevation); + const closestDistance = getClosestDistance(distance); + + onChange?.({ + azimuth: closestAzimuth.value, + elevation: closestElevation.value, + distance: closestDistance.value, + azimuthDegree: presetAzimuth, + elevationDegree: presetElevation, + distanceFactor: distance, + }); + }, + [distance, getClosestAzimuth, getClosestElevation, getClosestDistance, onChange] + ); + + const currentAzimuth = getClosestAzimuth(azimuth); + const currentElevation = getClosestElevation(elevation); + const currentDistance = getClosestDistance(distance); + + return ( +
+ {/* 3D 视图 */} +
+ + + + + + + {/* 当前角度显示 */} +
+ 方位: {currentAzimuth.label} ({azimuth}°) + 仰角: {currentElevation.label} ({elevation}°) +
+ + {/* 操作提示 */} +
+ 🖱️ 拖拽旋转视角 +
+
+ + {/* 快速预设按钮 */} +
+
快速选择
+
+ {[ + { label: "正前", azimuth: 0, elevation: 0 }, + { label: "前3/4", azimuth: 45, elevation: 0 }, + { label: "侧面", azimuth: 90, elevation: 0 }, + { label: "后3/4", azimuth: 135, elevation: 0 }, + { label: "正后", azimuth: 180, elevation: 0 }, + { label: "俯视", azimuth: 0, elevation: 60 }, + { label: "仰视", azimuth: 0, elevation: -30 }, + { label: "鸟瞰", azimuth: 45, elevation: 60 }, + ].map((preset) => ( + + ))} +
+
+ + {/* 距离控制 */} +
+
镜头距离
+
+ {DISTANCE_OPTIONS.map((opt) => ( + + ))} +
+
+
+ ); +} + +export default AngleController3D; +``` + +### 4.5 角度编辑对话框组件 + +**文件:`components/angle-edit-dialog.tsx`** + +```tsx +"use client"; + +import { useState, useCallback } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import { Label } from "@/components/ui/label"; +import { Loader2, Download, Check, RotateCcw } from "lucide-react"; +import { toast } from "sonner"; +import { AngleController3D } from "./angle-controller-3d"; +import { cn } from "@/lib/utils"; + +interface AngleEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + imageUrl: string; + onComplete?: (resultUrl: string) => void; +} + +interface AngleParams { + azimuth: string; + elevation: string; + distance: string; + azimuthDegree: number; + elevationDegree: number; + distanceFactor: number; +} + +export function AngleEditDialog({ + open, + onOpenChange, + imageUrl, + onComplete, +}: AngleEditDialogProps) { + const [isProcessing, setIsProcessing] = useState(false); + const [resultUrl, setResultUrl] = useState(null); + const [loraScale, setLoraScale] = useState(0.9); + const [currentAngles, setCurrentAngles] = useState({ + azimuth: "front-right", + elevation: "eye-level", + distance: "medium", + azimuthDegree: 45, + elevationDegree: 0, + distanceFactor: 1.0, + }); + + // 处理角度变化 + const handleAngleChange = useCallback((angles: AngleParams) => { + setCurrentAngles(angles); + // 清除之前的结果,因为角度变了 + setResultUrl(null); + }, []); + + // 生成新视角 + const handleGenerate = useCallback(async () => { + setIsProcessing(true); + setResultUrl(null); + + try { + const response = await fetch("/api/angle-edit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + image_url: imageUrl, + azimuth: currentAngles.azimuth, + elevation: currentAngles.elevation, + distance: currentAngles.distance, + lora_scale: loraScale, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "生成失败"); + } + + const data = await response.json(); + setResultUrl(data.image_url); + toast.success("视角生成成功!"); + } catch (error) { + console.error("角度编辑失败:", error); + toast.error(error instanceof Error ? error.message : "生成失败,请重试"); + } finally { + setIsProcessing(false); + } + }, [imageUrl, currentAngles, loraScale]); + + // 下载结果图片 + const handleDownload = useCallback(async () => { + if (!resultUrl) return; + + try { + const response = await fetch(resultUrl); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `angle-edit-${currentAngles.azimuth}-${currentAngles.elevation}-${Date.now()}.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success("图片已下载"); + } catch { + toast.error("下载失败"); + } + }, [resultUrl, currentAngles]); + + // 使用此图片 + const handleUseImage = useCallback(() => { + if (resultUrl) { + onComplete?.(resultUrl); + onOpenChange(false); + } + }, [resultUrl, onComplete, onOpenChange]); + + // 重置 + const handleReset = useCallback(() => { + setResultUrl(null); + setCurrentAngles({ + azimuth: "front-right", + elevation: "eye-level", + distance: "medium", + azimuthDegree: 45, + elevationDegree: 0, + distanceFactor: 1.0, + }); + setLoraScale(0.9); + }, []); + + return ( + + + + + 🎯 多角度编辑 + + + 拖拽 3D 控制器选择相机角度,AI 将生成对应视角的图像 + + + +
+ {/* 左侧:3D 控制器 */} +
+ + + {/* LoRA 强度控制 */} +
+
+ + + {(loraScale * 100).toFixed(0)}% + +
+ setLoraScale(v)} + min={0.5} + max={1.0} + step={0.05} + /> +

+ 值越高角度变化越明显,值越低保留原图特征越多 +

+
+
+ + {/* 右侧:图片预览 */} +
+ {/* 原图 */} +
+ +
+ 原图 +
+
+ + {/* 结果图 */} +
+ +
+ {isProcessing ? ( +
+ +

+ 正在生成新视角... +

+

+ 预计需要 10-30 秒 +

+
+ ) : resultUrl ? ( + 生成结果 + ) : ( +
+

选择角度后点击"生成新视角"

+
+ )} +
+
+
+
+ + {/* 操作按钮 */} +
+ + +
+ {resultUrl && ( + <> + + + + )} + {!resultUrl && ( + + )} +
+
+
+
+ ); +} + +export default AngleEditDialog; +``` + +### 4.6 集成到图片编辑器 + +**文件:`components/image-annotator.tsx`** + +在工具栏中添加角度编辑按钮: + +```tsx +// 在文件顶部导入 +import { AngleEditDialog } from "./angle-edit-dialog"; +import { Move3D } from "lucide-react"; + +// 在组件内添加状态 +const [isAngleEditOpen, setIsAngleEditOpen] = useState(false); + +// 在工具栏添加按钮 + + +// 添加对话框组件 + { + // 使用生成的新视角图片 + setCurrentImageUrl(resultUrl); + }} +/> +``` + +### 4.7 集成到聊天图片预览(与抠图按钮并列) + +**文件:`components/image-preview.tsx`** + +在聊天对话中的图片上添加"角度调整"按钮,与现有的"变清晰"、"抠图"、"图像拓展"按钮并列: + +```tsx +// 1. 在文件顶部添加导入 +import { Move3D } from "lucide-react"; +import { AngleEditDialog } from "./angle-edit-dialog"; + +// 2. 修改组件 Props 接口,添加角度编辑相关属性 +export function ImagePreview({ + src, + alt, + onImageEdit, + onAnnotate, + onDrawingComplete, + onImageEnhance, + onImageRmbg, + onImageExtend, + onImageAngleEdit, // 新增:角度编辑回调 + isProcessingEnhance, + isProcessingRmbg, + isProcessingExtend, + isProcessingAngleEdit, // 新增:角度编辑处理状态 + imageIndex +}: { + // ... 其他 props ... + onImageAngleEdit?: (imageUrl: string) => void; + isProcessingAngleEdit?: boolean; +}) { + // 3. 添加角度编辑对话框状态 + const [showAngleEditDialog, setShowAngleEditDialog] = useState(false); + + // 4. 在左上角按钮组中添加"角度调整"按钮(与抠图按钮并列) + // 位置:在 onImageExtend 按钮之后 + {onImageAngleEdit && ( + + )} + + // 5. 在组件底部添加角度编辑对话框 + {showAngleEditDialog && onImageAngleEdit && ( + { + onImageAngleEdit(resultUrl); + setShowAngleEditDialog(false); + toast.success("角度调整图片已生成"); + }} + /> + )} +} +``` + +**按钮布局效果**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [变清晰] [抠图] [图像拓展] [角度调整] [尺寸调整] │ ← 左上角按钮组 +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 生成的图片 │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ [画图] [编辑] [下载] │ ← 右下角悬停按钮 +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 💰 五、成本估算 + +### 5.1 fal.ai API 定价 + +| 项目 | 费用 | +|------|------| +| 每张图片处理 | ~$0.02-0.05 | +| 首次充值赠送 | $10 免费额度 | + +### 5.2 月度成本预估 + +| 月处理量 | 预估成本 | +|---------|---------| +| 500 张/月 | ¥70-175 | +| 1,000 张/月 | ¥140-350 | +| 5,000 张/月 | ¥700-1,750 | +| 10,000 张/月 | ¥1,400-3,500 | + +--- + +## ⏱️ 六、实施时间表 + +| 阶段 | 任务 | 预计时间 | +|------|------|---------| +| **阶段一** | 管理后台配置 | 0.5 天 | +| | - 添加配置状态和 UI | 2 小时 | +| | - 实现配置保存/读取 | 2 小时 | +| **阶段二** | 后端 API | 0.5 天 | +| | - 创建 API 路由 | 2 小时 | +| | - 配置读取逻辑 | 1 小时 | +| | - fal.ai 集成 | 1 小时 | +| **阶段三** | 前端组件 (含 3D 控制器) | 1.5 天 | +| | - 3D 角度控制器组件 (Three.js) | 4 小时 | +| | - 角度编辑对话框组件 | 3 小时 | +| | - 集成到图片编辑器 | 2 小时 | +| | - 3D 交互优化与测试 | 3 小时 | +| **阶段四** | 测试优化 | 0.5 天 | +| | - 功能测试 | 2 小时 | +| | - UI/UX 优化 | 2 小时 | +| **总计** | | **3 天** | + +--- + +## 🚀 七、快速开始 + +### 7.1 获取 fal.ai API Key + +1. 访问 https://fal.ai/dashboard/keys +2. 注册/登录账户 +3. 创建新的 API Key +4. 复制保存 + +### 7.2 安装依赖 + +```bash +# 安装 fal.ai SDK +npm install @fal-ai/serverless-client + +# 安装 Three.js 和 React Three Fiber(3D 控制器) +npm install @react-three/fiber @react-three/drei three +npm install -D @types/three +``` + +### 7.3 创建文件 + +按顺序创建以下文件: + +1. `app/api/angle-edit/route.ts` - 后端 API +2. `components/angle-controller-3d.tsx` - 3D 角度控制器 (Three.js) +3. `components/angle-edit-dialog.tsx` - 编辑对话框 + +### 7.4 添加管理后台配置 + +修改 `app/(admin)/settings/admin-settings-client.tsx`,添加角度编辑配置卡片。 + +### 7.5 集成到编辑器 + +修改 `components/image-annotator.tsx`,添加角度编辑入口按钮。 + +### 7.6 管理员配置 + +1. 登录管理后台 +2. 进入系统设置页面 +3. 找到"角度编辑配置"卡片 +4. 填入 fal.ai API Key +5. 开启功能 +6. 保存配置 + +--- + +## ⚠️ 八、注意事项 + +### 8.1 配置安全 + +- API Key 存储在数据库中,通过管理后台配置 +- 前端无法直接访问 API Key +- 建议定期轮换 API Key + +### 8.2 使用限制 + +- **图片格式**:支持 JPG、PNG、WebP +- **图片大小**:可在管理后台配置(默认 10MB) +- **处理时间**:通常 10-30 秒 +- **并发限制**:取决于 fal.ai 账户等级 + +### 8.3 效果说明 + +- 适合主体清晰、背景简单的图片 +- 汽车、产品、角色等物体效果最佳 +- 复杂场景可能效果不理想 +- 生成的是 AI 估算视角,非真实 3D 渲染 + +--- + +## 📎 九、参考资料 + +- [fal.ai 官方文档](https://fal.ai/docs) +- [fal.ai JavaScript SDK](https://www.npmjs.com/package/@fal-ai/serverless-client) +- [Hugging Face 模型页](https://huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA) +- [Qwen-Image-Edit 官方介绍](https://www.qwenimagen.com/qwen-image-edit) + +--- + +## 📖 附录:官方 96 个提示词完整列表 + +以下是来自官方 README 的完整 96 个提示词,可供参考使用: + +### CLOSE-UP (32 prompts) + +
+点击展开 + +**Low-angle (-30°)** +``` + front view low-angle shot close-up + front-right quarter view low-angle shot close-up + right side view low-angle shot close-up + back-right quarter view low-angle shot close-up + back view low-angle shot close-up + back-left quarter view low-angle shot close-up + left side view low-angle shot close-up + front-left quarter view low-angle shot close-up +``` + +**Eye-level (0°)** +``` + front view eye-level shot close-up + front-right quarter view eye-level shot close-up + right side view eye-level shot close-up + back-right quarter view eye-level shot close-up + back view eye-level shot close-up + back-left quarter view eye-level shot close-up + left side view eye-level shot close-up + front-left quarter view eye-level shot close-up +``` + +**Elevated (30°)** +``` + front view elevated shot close-up + front-right quarter view elevated shot close-up + right side view elevated shot close-up + back-right quarter view elevated shot close-up + back view elevated shot close-up + back-left quarter view elevated shot close-up + left side view elevated shot close-up + front-left quarter view elevated shot close-up +``` + +**High-angle (60°)** +``` + front view high-angle shot close-up + front-right quarter view high-angle shot close-up + right side view high-angle shot close-up + back-right quarter view high-angle shot close-up + back view high-angle shot close-up + back-left quarter view high-angle shot close-up + left side view high-angle shot close-up + front-left quarter view high-angle shot close-up +``` + +
+ +### MEDIUM SHOT (32 prompts) + +
+点击展开 + +**Low-angle (-30°)** +``` + front view low-angle shot medium shot + front-right quarter view low-angle shot medium shot + right side view low-angle shot medium shot + back-right quarter view low-angle shot medium shot + back view low-angle shot medium shot + back-left quarter view low-angle shot medium shot + left side view low-angle shot medium shot + front-left quarter view low-angle shot medium shot +``` + +**Eye-level (0°)** — 参考姿态: `front view eye-level shot medium shot` +``` + front view eye-level shot medium shot + front-right quarter view eye-level shot medium shot + right side view eye-level shot medium shot + back-right quarter view eye-level shot medium shot + back view eye-level shot medium shot + back-left quarter view eye-level shot medium shot + left side view eye-level shot medium shot + front-left quarter view eye-level shot medium shot +``` + +**Elevated (30°)** +``` + front view elevated shot medium shot + front-right quarter view elevated shot medium shot + right side view elevated shot medium shot + back-right quarter view elevated shot medium shot + back view elevated shot medium shot + back-left quarter view elevated shot medium shot + left side view elevated shot medium shot + front-left quarter view elevated shot medium shot +``` + +**High-angle (60°)** +``` + front view high-angle shot medium shot + front-right quarter view high-angle shot medium shot + right side view high-angle shot medium shot + back-right quarter view high-angle shot medium shot + back view high-angle shot medium shot + back-left quarter view high-angle shot medium shot + left side view high-angle shot medium shot + front-left quarter view high-angle shot medium shot +``` + +
+ +### WIDE SHOT (32 prompts) + +
+点击展开 + +**Low-angle (-30°)** +``` + front view low-angle shot wide shot + front-right quarter view low-angle shot wide shot + right side view low-angle shot wide shot + back-right quarter view low-angle shot wide shot + back view low-angle shot wide shot + back-left quarter view low-angle shot wide shot + left side view low-angle shot wide shot + front-left quarter view low-angle shot wide shot +``` + +**Eye-level (0°)** +``` + front view eye-level shot wide shot + front-right quarter view eye-level shot wide shot + right side view eye-level shot wide shot + back-right quarter view eye-level shot wide shot + back view eye-level shot wide shot + back-left quarter view eye-level shot wide shot + left side view eye-level shot wide shot + front-left quarter view eye-level shot wide shot +``` + +**Elevated (30°)** +``` + front view elevated shot wide shot + front-right quarter view elevated shot wide shot + right side view elevated shot wide shot + back-right quarter view elevated shot wide shot + back view elevated shot wide shot + back-left quarter view elevated shot wide shot + left side view elevated shot wide shot + front-left quarter view elevated shot wide shot +``` + +**High-angle (60°)** +``` + front view high-angle shot wide shot + front-right quarter view high-angle shot wide shot + right side view high-angle shot wide shot + back-right quarter view high-angle shot wide shot + back view high-angle shot wide shot + back-left quarter view high-angle shot wide shot + left side view high-angle shot wide shot + front-left quarter view high-angle shot wide shot +``` + +
+ +--- + +*文档更新时间:2026-01-10* +*版本:v3.0 - API 集成方案(数据库配置版 + 3D 控制器)* diff --git a/package.json b/package.json index d82f4e9..57c2c3e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@codemirror/state": "^6.5.0", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.35.3", + "@fal-ai/serverless-client": "^0.15.0", "@icons-pack/react-simple-icons": "^13.7.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", @@ -40,6 +41,8 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-use-controllable-state": "^1.2.2", "@radix-ui/react-visually-hidden": "^1.1.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@types/form-data": "^2.5.2", "@vercel/analytics": "^1.3.1", "@vercel/blob": "^0.24.1", @@ -95,6 +98,8 @@ "swr": "^2.2.5", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "three": "^0.182.0", + "three-stdlib": "^2.36.1", "tokenlens": "1.3.0", "use-stick-to-bottom": "^1.1.1", "usehooks-ts": "^3.1.0", @@ -113,6 +118,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "@types/sharp": "^0.32.0", + "@types/three": "^0.182.0", "drizzle-kit": "^0.25.0", "postcss": "^8", "tailwindcss": "^4.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab83fbb..b70355b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@codemirror/view': specifier: ^6.35.3 version: 6.36.4 + '@fal-ai/serverless-client': + specifier: ^0.15.0 + version: 0.15.0 '@icons-pack/react-simple-icons': specifier: ^13.7.0 version: 13.8.0(react@19.0.0-rc-45804af1-20241021) @@ -74,6 +77,12 @@ importers: '@radix-ui/react-visually-hidden': specifier: ^1.1.0 version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@react-three/drei': + specifier: ^10.7.7 + version: 10.7.7(@react-three/fiber@9.5.0(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0))(@types/react@18.3.18)(@types/three@0.182.0)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0) + '@react-three/fiber': + specifier: ^9.5.0 + version: 9.5.0(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0) '@types/form-data': specifier: ^2.5.2 version: 2.5.2 @@ -239,6 +248,12 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.1.16) + three: + specifier: ^0.182.0 + version: 0.182.0 + three-stdlib: + specifier: ^2.36.1 + version: 2.36.1(three@0.182.0) tokenlens: specifier: 1.3.0 version: 1.3.0 @@ -288,6 +303,9 @@ importers: '@types/sharp': specifier: ^0.32.0 version: 0.32.0 + '@types/three': + specifier: ^0.182.0 + version: 0.182.0 drizzle-kit: specifier: ^0.25.0 version: 0.25.0 @@ -685,6 +703,9 @@ packages: '@codemirror/view@6.36.4': resolution: {integrity: sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -1119,6 +1140,11 @@ packages: cpu: [x64] os: [win32] + '@fal-ai/serverless-client@0.15.0': + resolution: {integrity: sha512-4Vuocu0342OijAN6xO/lwohDV7h90LbkTnOAEwH+pYvMFVC6RYmHS4GILc/wnOWBTw+iFlZFEKlljEVolkjVfg==} + engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -1332,9 +1358,21 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + + '@msgpack/msgpack@3.1.3': + resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} + engines: {node: '>= 18'} + '@neondatabase/serverless@0.9.5': resolution: {integrity: sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==} @@ -2218,6 +2256,42 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-three/drei@10.7.7': + resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.5.0': + resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@redis/bloom@5.9.0': resolution: {integrity: sha512-W9D8yfKTWl4tP8lkC3MRYkMz4OfbuzE/W8iObe0jFgoRmgMfkBV+Vj38gvIqZPImtY0WB34YZkX3amYuQebvRQ==} engines: {node: '>= 18'} @@ -2709,6 +2783,9 @@ packages: peerDependencies: typescript: '>=5.7.2' + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2811,6 +2888,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2857,6 +2937,9 @@ packages: '@types/node@22.13.10': resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/omelette@0.4.5': resolution: {integrity: sha512-zUCJpVRwfMcZfkxSCGp73mgd3/xesvPz5tQJIORlfP/zkYEyp9KUfF7IP3RRjyZR3DwxkPs96/IFf70GmYZYHQ==} @@ -2877,6 +2960,11 @@ packages: peerDependencies: '@types/react': ^18.0.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -2890,6 +2978,12 @@ packages: '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.182.0': + resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2899,9 +2993,20 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vercel/analytics@1.5.0': resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} peerDependencies: @@ -2998,6 +3103,9 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@webgpu/types@0.1.68': + resolution: {integrity: sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==} + '@xenova/transformers@2.17.2': resolution: {integrity: sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==} @@ -3043,6 +3151,9 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.11: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true @@ -3051,12 +3162,18 @@ packages: resolution: {integrity: sha512-2FcgD12xPbwCoe5i9/HK0jJ1xA1m+QfC1e6htG9Bl/hNOnLyaFmQSlqLKcfe3QdnoMPKpKEGFCbESBTg+SJNOw==} engines: {node: '>=18'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bowser@2.13.1: resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bufferutil@4.0.9: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} @@ -3073,6 +3190,12 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001704: resolution: {integrity: sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==} @@ -3183,6 +3306,15 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3393,6 +3525,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -3417,6 +3552,9 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + drizzle-kit@0.25.0: resolution: {integrity: sha512-Rcf0nYCAKizwjWQCY+d3zytyuTbDb81NcaPor+8NebESlUz1+9W3uGl0+r9FhU4Qal5Zv9j/7neXCSCe7DHzjA==} hasBin: true @@ -3588,6 +3726,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventsource-parser@1.1.2: + resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} + engines: {node: '>=14.18'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -3621,6 +3763,12 @@ packages: picomatch: optional: true + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + flatbuffers@1.12.0: resolution: {integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==} @@ -3683,6 +3831,9 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3759,6 +3910,9 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3769,6 +3923,12 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} @@ -3818,6 +3978,17 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3854,6 +4025,9 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -3968,6 +4142,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4044,6 +4224,14 @@ packages: mermaid@11.12.1: resolution: {integrity: sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g==} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.22.0: + resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -4267,6 +4455,10 @@ packages: path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -4367,6 +4559,9 @@ packages: resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} engines: {node: '>=12'} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: @@ -4383,6 +4578,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -4519,6 +4717,15 @@ packages: peerDependencies: react: '>= 0.14.0' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.0.0-rc-45804af1-20241021: resolution: {integrity: sha512-RYkYXmX7Iia56DQJaNK0Ribh5uF/sDBNirtNNHNNqqa3wpIlcFOkRvPTW3RSwAXmBVnEZlKbeaMhlBT9p52lqA==} engines: {node: '>=0.10.0'} @@ -4563,6 +4770,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -4582,6 +4793,9 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + robot3@0.4.1: + resolution: {integrity: sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -4605,6 +4819,9 @@ packages: scheduler@0.25.0-rc-45804af1-20241021: resolution: {integrity: sha512-8jyu/iy3tGFNakMMCWnKw/vsiTcapDyl0LKlZ3fUKBcBicZAkrrCC1bdqVFx0Ioxgry1SzOrCGcZLM7vtWK00A==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -4617,6 +4834,14 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shiki@3.14.0: resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} @@ -4655,6 +4880,15 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -4701,6 +4935,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + swr@2.3.3: resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} peerDependencies: @@ -4724,6 +4963,19 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.182.0: + resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -4759,6 +5011,19 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -4787,6 +5052,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -4877,6 +5145,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -4989,6 +5261,17 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -5026,6 +5309,39 @@ packages: zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5756,6 +6072,8 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@dimforge/rapier3d-compat@0.12.0': {} + '@drizzle-team/brocli@0.10.2': {} '@emnapi/runtime@1.7.1': @@ -5983,6 +6301,12 @@ snapshots: '@esbuild/win32-x64@0.25.1': optional: true + '@fal-ai/serverless-client@0.15.0': + dependencies: + '@msgpack/msgpack': 3.1.3 + eventsource-parser: 1.1.2 + robot3: 0.4.1 + '@fastify/busboy@2.1.1': {} '@floating-ui/core@1.6.9': @@ -6167,10 +6491,19 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@mediapipe/tasks-vision@0.10.17': {} + '@mermaid-js/parser@0.6.3': dependencies: langium: 3.3.1 + '@monogrid/gainmap-js@3.4.0(three@0.182.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.182.0 + + '@msgpack/msgpack@3.1.3': {} + '@neondatabase/serverless@0.9.5': dependencies: '@types/pg': 8.11.6 @@ -7068,6 +7401,59 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0))(@types/react@18.3.18)(@types/three@0.182.0)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.182.0) + '@react-three/fiber': 9.5.0(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0) + '@use-gesture/react': 10.3.1(react@19.0.0-rc-45804af1-20241021) + camera-controls: 3.1.2(three@0.182.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.182.0)(three@0.182.0) + meshline: 3.3.1(three@0.182.0) + react: 19.0.0-rc-45804af1-20241021 + stats-gl: 2.4.2(@types/three@0.182.0)(three@0.182.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.0.0-rc-45804af1-20241021) + three: 0.182.0 + three-mesh-bvh: 0.8.3(three@0.182.0) + three-stdlib: 2.36.1(three@0.182.0) + troika-three-text: 0.52.4(three@0.182.0) + tunnel-rat: 0.1.2(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + use-sync-external-store: 1.6.0(react@19.0.0-rc-45804af1-20241021) + utility-types: 3.11.0 + zustand: 5.0.9(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021)(use-sync-external-store@1.6.0(react@19.0.0-rc-45804af1-20241021)) + optionalDependencies: + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.5.0(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + react: 19.0.0-rc-45804af1-20241021 + react-use-measure: 2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.0.0-rc-45804af1-20241021) + three: 0.182.0 + use-sync-external-store: 1.6.0(react@19.0.0-rc-45804af1-20241021) + zustand: 5.0.9(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021)(use-sync-external-store@1.6.0(react@19.0.0-rc-45804af1-20241021)) + optionalDependencies: + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + transitivePeerDependencies: + - '@types/react' + - immer + '@redis/bloom@5.9.0(@redis/client@5.9.0)': dependencies: '@redis/client': 5.9.0 @@ -7627,6 +8013,8 @@ snapshots: dependencies: typescript: 5.8.2 + '@tweenjs/tween.js@23.1.3': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -7755,6 +8143,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/draco3d@1.4.10': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.6 @@ -7800,6 +8190,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/omelette@0.4.5': {} '@types/papaparse@5.3.15': @@ -7821,6 +8213,10 @@ snapshots: dependencies: '@types/react': 18.3.18 + '@types/react-reconciler@0.28.9(@types/react@18.3.18)': + dependencies: + '@types/react': 18.3.18 + '@types/react-syntax-highlighter@15.5.13': dependencies: '@types/react': 18.3.18 @@ -7836,6 +8232,18 @@ snapshots: '@types/shimmer@1.2.0': {} + '@types/stats.js@0.17.4': {} + + '@types/three@0.182.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.68 + fflate: 0.8.2 + meshoptimizer: 0.22.0 + '@types/trusted-types@2.0.7': optional: true @@ -7843,8 +8251,17 @@ snapshots: '@types/unist@3.0.3': {} + '@types/webxr@0.5.24': {} + '@ungap/structured-clone@1.3.0': {} + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.0.0-rc-45804af1-20241021)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.0.0-rc-45804af1-20241021 + '@vercel/analytics@1.5.0(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': optionalDependencies: next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) @@ -7933,6 +8350,8 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@webgpu/types@0.1.68': {} + '@xenova/transformers@2.17.2': dependencies: '@huggingface/jinja': 0.2.2 @@ -7979,14 +8398,25 @@ snapshots: bail@2.0.2: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.9.11: {} bcrypt-ts@5.0.3: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bowser@2.13.1: {} buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bufferutil@4.0.9: dependencies: node-gyp-build: 4.8.4 @@ -8001,6 +8431,10 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + camera-controls@3.1.2(three@0.182.0): + dependencies: + three: 0.182.0 + caniuse-lite@1.0.30001704: {} ccount@2.0.1: {} @@ -8101,6 +8535,16 @@ snapshots: crelt@1.0.6: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -8317,6 +8761,10 @@ snapshots: dequal@2.0.3: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.0.3: {} detect-libc@2.1.2: {} @@ -8335,6 +8783,8 @@ snapshots: dotenv@16.4.7: {} + draco3d@1.5.7: {} + drizzle-kit@0.25.0: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -8492,6 +8942,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + eventsource-parser@1.1.2: {} + eventsource-parser@3.0.6: {} expect-type@1.2.2: {} @@ -8514,6 +8966,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.6.10: {} + + fflate@0.8.2: {} + flatbuffers@1.12.0: {} form-data@4.0.5: @@ -8573,6 +9029,8 @@ snapshots: globals@15.15.0: {} + glsl-noise@0.0.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -8725,6 +9183,8 @@ snapshots: highlightjs-vue@1.0.0: {} + hls.js@1.6.15: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -8733,6 +9193,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + + immediate@3.0.6: {} + import-in-the-middle@1.15.0: dependencies: acorn: 8.15.0 @@ -8776,6 +9240,17 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@2.2.2: {} + + isexe@2.0.0: {} + + its-fine@2.0.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@18.3.18) + react: 19.0.0-rc-45804af1-20241021 + transitivePeerDependencies: + - '@types/react' + jiti@2.6.1: {} jose@6.1.3: {} @@ -8806,6 +9281,10 @@ snapshots: layout-base@2.0.1: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.30.2: optional: true @@ -8894,6 +9373,11 @@ snapshots: dependencies: react: 19.0.0-rc-45804af1-20241021 + maath@0.10.8(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9107,6 +9591,12 @@ snapshots: transitivePeerDependencies: - supports-color + meshline@3.3.1(three@0.182.0): + dependencies: + three: 0.182.0 + + meshoptimizer@0.22.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.1.0 @@ -9449,6 +9939,8 @@ snapshots: path-data-parser@0.1.0: {} + path-key@3.1.1: {} + path-parse@1.0.7: {} pathe@2.0.3: {} @@ -9552,6 +10044,8 @@ snapshots: postgres@3.4.5: {} + potpack@1.0.2: {} + preact-render-to-string@6.5.11(preact@10.24.3): dependencies: preact: 10.24.3 @@ -9562,6 +10056,11 @@ snapshots: prismjs@1.30.0: {} + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + property-information@5.6.0: dependencies: xtend: 4.0.2 @@ -9815,6 +10314,12 @@ snapshots: react: 19.0.0-rc-45804af1-20241021 refractor: 3.6.0 + react-use-measure@2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021): + dependencies: + react: 19.0.0-rc-45804af1-20241021 + optionalDependencies: + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + react@19.0.0-rc-45804af1-20241021: {} redis@5.9.0: @@ -9902,6 +10407,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: dependencies: debug: 4.4.3 @@ -9922,6 +10429,8 @@ snapshots: retry@0.13.1: {} + robot3@0.4.1: {} + robust-predicates@3.0.2: {} rollup@4.52.5: @@ -9967,6 +10476,8 @@ snapshots: scheduler@0.25.0-rc-45804af1-20241021: {} + scheduler@0.27.0: {} + semver@7.7.3: {} server-only@0.0.1: {} @@ -10002,6 +10513,12 @@ snapshots: '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shiki@3.14.0: dependencies: '@shikijs/core': 3.14.0 @@ -10039,6 +10556,13 @@ snapshots: stackback@0.0.2: {} + stats-gl@2.4.2(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + + stats.js@0.17.0: {} + std-env@3.10.0: {} streamdown@1.4.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021): @@ -10091,6 +10615,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.0.0-rc-45804af1-20241021): + dependencies: + react: 19.0.0-rc-45804af1-20241021 + swr@2.3.3(react@19.0.0-rc-45804af1-20241021): dependencies: dequal: 2.0.3 @@ -10109,6 +10637,22 @@ snapshots: tapable@2.2.1: {} + three-mesh-bvh@0.8.3(three@0.182.0): + dependencies: + three: 0.182.0 + + three-stdlib@2.36.1(three@0.182.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.182.0 + + three@0.182.0: {} + throttleit@2.1.0: {} tinybench@2.9.0: {} @@ -10137,6 +10681,20 @@ snapshots: trim-lines@3.0.1: {} + troika-three-text@0.52.4(three@0.182.0): + dependencies: + bidi-js: 1.0.3 + three: 0.182.0 + troika-three-utils: 0.52.4(three@0.182.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.182.0): + dependencies: + three: 0.182.0 + + troika-worker-utils@0.52.0: {} + trough@2.2.0: {} trpc-cli@0.10.2(typescript@5.8.2): @@ -10161,6 +10719,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-rat@0.1.2(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021): + dependencies: + zustand: 4.5.7(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + transitivePeerDependencies: + - '@types/react' + - immer + - react + typescript@5.8.2: {} uc.micro@2.1.0: {} @@ -10283,6 +10849,8 @@ snapshots: util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + uuid@11.1.0: {} vfile-location@5.0.3: @@ -10400,6 +10968,14 @@ snapshots: web-namespaces@2.0.1: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -10423,4 +10999,17 @@ snapshots: zod@4.1.12: {} + zustand@4.5.7(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021): + dependencies: + use-sync-external-store: 1.6.0(react@19.0.0-rc-45804af1-20241021) + optionalDependencies: + '@types/react': 18.3.18 + react: 19.0.0-rc-45804af1-20241021 + + zustand@5.0.9(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021)(use-sync-external-store@1.6.0(react@19.0.0-rc-45804af1-20241021)): + optionalDependencies: + '@types/react': 18.3.18 + react: 19.0.0-rc-45804af1-20241021 + use-sync-external-store: 1.6.0(react@19.0.0-rc-45804af1-20241021) + zwitch@2.0.4: {} diff --git a/scripts/fix-high-cpu.sh b/scripts/fix-high-cpu.sh index e6fdd78..74e391b 100755 --- a/scripts/fix-high-cpu.sh +++ b/scripts/fix-high-cpu.sh @@ -86,3 +86,5 @@ echo "" + + diff --git a/系统性能诊断与解决方案.md b/系统性能诊断与解决方案.md index 7c3fd7a..036aec0 100644 --- a/系统性能诊断与解决方案.md +++ b/系统性能诊断与解决方案.md @@ -139,3 +139,6 @@ pmset -g + + +