From 4120432b1b8753268982cbe98ee66bd8e3e2e82c Mon Sep 17 00:00:00 2001 From: uswiii Date: Thu, 16 Apr 2026 23:38:46 +0500 Subject: [PATCH 01/12] feat: Sketch to CAD -- upload page, workflow, hook, and routing - Replace CAD to Catalog card in CADStudio with Sketch to CAD (PenTool icon, /sketch-to-cad route) - Add /sketch-to-cad route in App.tsx behind ProtectedRoute + CADGate - sketch-to-cad-workflows.ts: workflow constants and buildSketchToCadStartBody for sketch_generate_v1 - use-sketch-to-cad-generation.ts: credit preflight, base64 data URI upload, poll + parse GLB result - SketchToCAD.tsx: multi-sketch upload page (max 10), per-sketch hint, credit estimate, mobile-responsive sticky footer - SketchLeftPanel.tsx + SketchUploadScreen.tsx: workspace components for sketch mode - posthog-events.ts: trackSketchToCadCompleted event - feature-flags.ts: isSketchToCadEnabled gate (allowlist, owner uswa@raresense.so) - credits-api.ts: sketch_generate_v1 fallback cost (85 credits) - sketch-to-cad-workflows.test.ts: 6 tests covering payload shape and return nodes --- src/App.tsx | 2 + .../sketch-to-cad/SketchLeftPanel.tsx | 168 +++++++ .../sketch-to-cad/SketchUploadScreen.tsx | 161 ++++++ src/hooks/use-sketch-to-cad-generation.ts | 187 +++++++ src/lib/credits-api.ts | 1 + src/lib/feature-flags.ts | 10 + src/lib/posthog-events.ts | 9 + src/lib/sketch-to-cad-workflows.test.ts | 55 +++ src/lib/sketch-to-cad-workflows.ts | 44 ++ src/pages/CADStudio.tsx | 16 +- src/pages/SketchToCAD.tsx | 466 ++++++++++++++++++ 11 files changed, 1111 insertions(+), 8 deletions(-) create mode 100644 src/components/sketch-to-cad/SketchLeftPanel.tsx create mode 100644 src/components/sketch-to-cad/SketchUploadScreen.tsx create mode 100644 src/hooks/use-sketch-to-cad-generation.ts create mode 100644 src/lib/sketch-to-cad-workflows.test.ts create mode 100644 src/lib/sketch-to-cad-workflows.ts create mode 100644 src/pages/SketchToCAD.tsx diff --git a/src/App.tsx b/src/App.tsx index acbef460..6913be05 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,6 +64,7 @@ const UnifiedStudio = lazyWithRetry(() => import("./pages/UnifiedStudio")); const CADStudio = lazyWithRetry(() => import("./pages/CADStudio")); const CADToCatalog = lazyWithRetry(() => import("./pages/CADToCatalog")); const TextToCAD = lazyWithRetry(() => import("./pages/TextToCAD")); +const SketchToCAD = lazyWithRetry(() => import("./pages/SketchToCAD")); const Generations = lazyWithRetry(() => import("./pages/Generations")); const Credits = lazyWithRetry(() => import("./pages/Credits")); const Pricing = lazyWithRetry(() => import("./pages/Pricing")); @@ -230,6 +231,7 @@ const App = () => ( } /> } /> } /> + } /> {/* Admin routes */} }> diff --git a/src/components/sketch-to-cad/SketchLeftPanel.tsx b/src/components/sketch-to-cad/SketchLeftPanel.tsx new file mode 100644 index 00000000..6b17aa0d --- /dev/null +++ b/src/components/sketch-to-cad/SketchLeftPanel.tsx @@ -0,0 +1,168 @@ +import { useRef, useCallback } from "react"; +import { ImagePlus } from "lucide-react"; +import creditCoinIcon from "@/assets/icons/credit-coin.png"; +import { useEstimatedCost } from "@/hooks/use-estimated-cost"; +import { SKETCH_TO_CAD_WORKFLOW } from "@/lib/sketch-to-cad-workflows"; + +interface SketchLeftPanelProps { + previewUrl: string | null; + description: string; + onDescriptionChange: (d: string) => void; + isGenerating: boolean; + hasModel: boolean; + onRegenerate: () => void; + onNewSketch: (file: File) => void; + onGlbUpload: (file: File) => void; + onReset?: () => void; + creditBlock?: React.ReactNode; +} + +export default function SketchLeftPanel({ + previewUrl, + description, + onDescriptionChange, + isGenerating, + hasModel, + onRegenerate, + onNewSketch, + onGlbUpload, + onReset, + creditBlock, +}: SketchLeftPanelProps) { + const sketchInputRef = useRef(null); + const glbInputRef = useRef(null); + const { cost: estimatedCost, loading: costLoading } = useEstimatedCost({ workflowName: SKETCH_TO_CAD_WORKFLOW }); + + const handleSketchInput = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file && file.type.startsWith('image/')) onNewSketch(file); + e.target.value = ''; + }, [onNewSketch]); + + const handleGlbInput = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) onGlbUpload(file); + e.target.value = ''; + }, [onGlbUpload]); + + return ( +
+ {/* Header */} +
+

+ Sketch to 3D +

+
+ + {/* Body */} +
+ {/* Sketch thumbnail */} +
+

+ Sketch +

+
+ {previewUrl ? ( + Your sketch + ) : ( +
+ +
+ )} +
+ + +
+ + {/* Description */} +
+

+ Description +

+