Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/areas/generate/components/GenerationHUD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default function GenerationHUD(): JSX.Element | null {
<div className="flex items-center gap-2.5">
<div className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
<span className="text-sm font-medium text-zinc-200">
{step ?? (status === 'uploading' ? 'Uploading image…' : 'Generating 3D mesh…')}
{step ?? (status === 'uploading' ? 'Reading image…' : 'Generating 3D mesh…')}
</span>
</div>
<span className="text-xs tabular-nums text-zinc-500">{formatElapsed(elapsed)}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/areas/generate/components/GenerationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function GenerationPanel(): JSX.Element {
const { status: jobStatus, progress, step, error } = currentJob

const statusLabel: Record<string, string> = {
uploading: 'Uploading image…',
uploading: 'Reading image…',
generating: step ?? 'Generating 3D mesh…',
done: 'Done!',
error: 'Generation failed',
Expand Down
28 changes: 22 additions & 6 deletions src/areas/generate/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,48 @@ import { useGeneration } from '@shared/hooks/useGeneration'

export default function ImageUpload(): JSX.Element {
const { currentJob } = useGeneration()
const { setSelectedImagePath, selectedImagePreviewUrl, setSelectedImagePreviewUrl } = useAppStore()
const { setSelectedImagePath, selectedImagePreviewUrl, setSelectedImagePreviewUrl, setSelectedImageData } = useAppStore()
const [isDragging, setIsDragging] = useState(false)

const isGenerating = currentJob?.status === 'uploading' || currentJob?.status === 'generating'

const handleFileSelect = useCallback(async () => {
const path = await window.electron.fs.selectImage()
if (!path) return
setSelectedImageData(null)
setSelectedImagePath(path)

// Read via IPC → blob URL (file:// blocked when served from localhost in dev)
const base64 = await window.electron.fs.readFileBase64(path)
const byteArray = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
const blob = new Blob([byteArray], { type: 'image/png' })
setSelectedImagePreviewUrl(URL.createObjectURL(blob))
}, [setSelectedImagePath, setSelectedImagePreviewUrl])
}, [setSelectedImagePath, setSelectedImagePreviewUrl, setSelectedImageData])

const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const file = e.dataTransfer.files[0]
if (!file || !file.type.startsWith('image/')) return
const url = URL.createObjectURL(file)
setSelectedImagePreviewUrl(url)
setSelectedImagePath((file as File & { path?: string }).path ?? null)
}, [setSelectedImagePath, setSelectedImagePreviewUrl])

setSelectedImagePreviewUrl(URL.createObjectURL(file))

const filePath = (file as File & { path?: string }).path
if (filePath) {
setSelectedImageData(null)
setSelectedImagePath(filePath)
} else {
// file.path unavailable (some Electron configs) — read directly via FileReader
const reader = new FileReader()
reader.onload = (ev) => {
const dataUrl = ev.target?.result as string
const base64 = dataUrl.split(',')[1]
setSelectedImageData(base64)
setSelectedImagePath('__blob__')
}
reader.readAsDataURL(file)
}
}, [setSelectedImagePath, setSelectedImagePreviewUrl, setSelectedImageData])

return (
<div className="flex flex-col p-4 gap-3">
Expand Down
5 changes: 3 additions & 2 deletions src/shared/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ export function useApi() {
imagePath: string,
options: GenerationOptions,
collection: string = 'Default',
imageData?: string,
): Promise<{ jobId: string }> {
// Read file via IPC (avoids file:// restrictions in the renderer)
const base64 = await window.electron.fs.readFileBase64(imagePath)
// Use provided base64 (drag & drop) or read from disk via IPC
const base64 = imageData ?? await window.electron.fs.readFileBase64(imagePath)
const byteArray = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
const blob = new Blob([byteArray], { type: 'image/png' })
const filename = imagePath.split(/[\\/]/).pop() ?? 'image.png'
Expand Down
4 changes: 2 additions & 2 deletions src/shared/hooks/useGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCollectionsStore } from '@shared/stores/collectionsStore'
import { useApi } from './useApi'

export function useGeneration() {
const { currentJob, setCurrentJob, updateCurrentJob, generationOptions } = useAppStore()
const { currentJob, setCurrentJob, updateCurrentJob, generationOptions, selectedImageData } = useAppStore()
const addToWorkspace = useCollectionsStore((s) => s.addToWorkspace)
const activeCollectionId = useCollectionsStore((s) => s.activeCollectionId)
const { generateFromImage, pollJobStatus } = useApi()
Expand All @@ -23,7 +23,7 @@ export function useGeneration() {
setCurrentJob(job)

try {
const { jobId } = await generateFromImage(imagePath, generationOptions, activeCollectionId)
const { jobId } = await generateFromImage(imagePath, generationOptions, activeCollectionId, selectedImageData ?? undefined)

updateCurrentJob({ status: 'generating', progress: 0 })

Expand Down
4 changes: 4 additions & 0 deletions src/shared/stores/appStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ interface AppState {
setSelectedImagePath: (path: string | null) => void
selectedImagePreviewUrl: string | null
setSelectedImagePreviewUrl: (url: string | null) => void
selectedImageData: string | null // base64 content for drag & drop (when path is unavailable)
setSelectedImageData: (data: string | null) => void

// Generation options
generationOptions: GenerationOptions
Expand Down Expand Up @@ -135,6 +137,8 @@ export const useAppStore = create<AppState>()(
setSelectedImagePath: (path) => set({ selectedImagePath: path }),
selectedImagePreviewUrl: null,
setSelectedImagePreviewUrl: (url) => set({ selectedImagePreviewUrl: url }),
selectedImageData: null,
setSelectedImageData: (data) => set({ selectedImageData: data }),
generationOptions: DEFAULT_OPTIONS,
meshStats: null,
setMeshStats: (stats) => set({ meshStats: stats }),
Expand Down