From a26e76638e6aa3250cda519188d77ede75559d7b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Apr 2026 12:14:57 +0800 Subject: [PATCH] feat: add ai-cad camera orientation cube --- app/api/ai-cad/workspace-utils.ts | 8 ++ app/canvas/[id]/model3d/page.tsx | 1 + components/ai-cad/ai-cad-workbench.tsx | 138 ++++++++++++++++++-- components/canvas/model3d/Model3DViewer.tsx | 66 +++++++++- components/canvas/world3d/World3DViewer.tsx | 1 + 5 files changed, 199 insertions(+), 15 deletions(-) diff --git a/app/api/ai-cad/workspace-utils.ts b/app/api/ai-cad/workspace-utils.ts index 38277896b..496f41a8f 100644 --- a/app/api/ai-cad/workspace-utils.ts +++ b/app/api/ai-cad/workspace-utils.ts @@ -12,6 +12,7 @@ const FINAL_TASK_STATUS = new Set([ ]); const DEFAULT_VIEWER_SETTINGS = { + cameraPreset: "iso", envPreset: "studio", lightIntensity: 1.1, autoRotate: true, @@ -55,6 +56,13 @@ export function normalizeViewerSettings( } return { + cameraPreset: + value.cameraPreset === "top" || + value.cameraPreset === "front" || + value.cameraPreset === "right" || + value.cameraPreset === "iso" + ? value.cameraPreset + : DEFAULT_VIEWER_SETTINGS.cameraPreset, envPreset: typeof value.envPreset === "string" && value.envPreset.trim() ? value.envPreset diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index 3276653f7..86c55c5b8 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -641,6 +641,7 @@ export default function CanvasModel3DPage() { // Viewer Settings const [viewerSettings, setViewerSettings] = useState({ + cameraPreset: "iso", envPreset: "city", lightIntensity: 1, autoRotate: false, diff --git a/components/ai-cad/ai-cad-workbench.tsx b/components/ai-cad/ai-cad-workbench.tsx index 41247cce8..20a8c6960 100644 --- a/components/ai-cad/ai-cad-workbench.tsx +++ b/components/ai-cad/ai-cad-workbench.tsx @@ -193,6 +193,7 @@ const FINAL_TASK_STATUS = new Set([ ]); const DEFAULT_VIEWER_SETTINGS: Model3DViewerSettings = { + cameraPreset: "iso", envPreset: "studio", lightIntensity: 1.1, autoRotate: true, @@ -1815,6 +1816,13 @@ function normalizeViewerSettings(value: unknown): Model3DViewerSettings { return { ...DEFAULT_VIEWER_SETTINGS, + cameraPreset: + value.cameraPreset === "top" || + value.cameraPreset === "front" || + value.cameraPreset === "right" || + value.cameraPreset === "iso" + ? value.cameraPreset + : DEFAULT_VIEWER_SETTINGS.cameraPreset, envPreset, lightIntensity: typeof value.lightIntensity === "number" && @@ -2174,6 +2182,107 @@ function AiCadToolbarGroup({ ); } +function AiCadOrientationCube({ + className, + onChange, + value, +}: { + className?: string; + onChange: (preset: Model3DViewerSettings["cameraPreset"]) => void; + value: Model3DViewerSettings["cameraPreset"]; +}) { + const getFaceClasses = (preset: Model3DViewerSettings["cameraPreset"]) => + cn( + "cursor-pointer transition-all duration-150 ease-out", + value === preset + ? "fill-[#e9f7ef] stroke-[#2f8f5b]" + : "fill-[#f7f3ea] stroke-[#c9c0ae] hover:fill-[#f1ebde]" + ); + + const getLabelClasses = (preset: Model3DViewerSettings["cameraPreset"]) => + cn( + "pointer-events-none select-none fill-[#6f695d] text-[5px] font-medium uppercase tracking-[0.08em]", + value === preset && "fill-[#1e5c3a]" + ); + + return ( +
+
+ + onChange("top")}> + + + Top + + + onChange("front")}> + + + Front + + + onChange("right")}> + + + Right + + + +
+ + +
+ ); +} + function _AiCadMessageActionButton({ icon: Icon, label, @@ -4126,6 +4235,16 @@ export function AiCadWorkbench() { } } + function handleViewerCameraPresetChange( + preset: Model3DViewerSettings["cameraPreset"] + ) { + setViewerSettings((current) => ({ + ...current, + autoRotate: false, + cameraPreset: preset, + })); + } + return (
@@ -5744,7 +5863,7 @@ export function AiCadWorkbench() { ) : null}
-
+
+
@@ -6623,13 +6748,10 @@ export function AiCadWorkbench() {
-
-
-
Top
-
Front
-
Right
-
-
+
从右侧输入设计意图,模型会直接出现在画布中。 diff --git a/components/canvas/model3d/Model3DViewer.tsx b/components/canvas/model3d/Model3DViewer.tsx index 9c1822cbf..ffa7f3e33 100644 --- a/components/canvas/model3d/Model3DViewer.tsx +++ b/components/canvas/model3d/Model3DViewer.tsx @@ -8,9 +8,9 @@ import { OrbitControls, useGLTF, } from "@react-three/drei"; -import { Canvas } from "@react-three/fiber"; +import { Canvas, useThree } from "@react-three/fiber"; import { AlertCircle, Box, ExternalLink, Loader2 } from "lucide-react"; -import React, { Suspense, useEffect, useMemo } from "react"; +import React, { Suspense, useEffect, useMemo, useRef } from "react"; import type { Mesh, Object3D } from "three"; import { Button } from "@/components/ui/button"; @@ -27,7 +27,10 @@ export type Model3DStats = { meshCount: number; }; +export type Model3DCameraPreset = "front" | "iso" | "right" | "top"; + export type Model3DViewerSettings = { + cameraPreset: Model3DCameraPreset; envPreset: | "city" | "sunset" @@ -55,6 +58,47 @@ type Model3DViewerProps = { onStatsChange?: (stats: Model3DStats) => void; }; +const CAMERA_PRESET_POSITIONS: Record< + Model3DCameraPreset, + [number, number, number] +> = { + front: [0, 1.1, 3.2], + iso: [2, 1.5, 2], + right: [3.2, 1.1, 0], + top: [0, 3.4, 0.001], +}; + +function CameraPresetController({ + controlsRef, + preset, +}: { + controlsRef: React.MutableRefObject; + preset: Model3DCameraPreset; +}) { + const { camera } = useThree(); + + useEffect(() => { + const [x, y, z] = + CAMERA_PRESET_POSITIONS[preset] || CAMERA_PRESET_POSITIONS.iso; + + camera.position.set(x, y, z); + if (preset === "top") { + camera.up.set(0, 0, -1); + } else { + camera.up.set(0, 1, 0); + } + camera.lookAt(0, 0, 0); + + const controls = controlsRef.current; + if (controls) { + controls.target.set(0, 0, 0); + controls.update(); + } + }, [camera, controlsRef, preset]); + + return null; +} + // eslint-disable-next-line class ViewerErrorBoundary extends React.Component< { children: React.ReactNode; fallback: React.ReactNode }, @@ -231,6 +275,7 @@ export function Model3DViewer({ onLayersChange, onStatsChange, }: Model3DViewerProps) { + const controlsRef = useRef(null); const resolvedModelUrl = useMemo(() => { const normalized = String(modelUrl || "").trim(); if (!normalized) return ""; @@ -248,10 +293,7 @@ export function Model3DViewer({ if (!modelUrl) { return ( - + ); } @@ -294,9 +336,18 @@ export function Model3DViewer({ } > + diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx index dba14e9ca..c0bc8093e 100644 --- a/components/canvas/world3d/World3DViewer.tsx +++ b/components/canvas/world3d/World3DViewer.tsx @@ -76,6 +76,7 @@ type World3DViewerProps = { }; const DEFAULT_MESH_SETTINGS: Model3DViewerSettings = { + cameraPreset: "iso", envPreset: "city", lightIntensity: 1, autoRotate: false,