Stair system (full stack): - New StairNode + StairSegmentNode schemas with flights, landings, L/U-shapes - StairSystem: geometry generation with throttled per-frame updates - Stair tool, edit system, panels, tree node, and renderers - Event bus types, scene registry, and command palette entries Scene graph utilities: - cloneLevelSubtree: deep-clone a level with remapped IDs - forkSceneGraph: clone + strip scan/guide nodes for project forking Core improvements: - Read-only mode on scene store (blocks create/update/delete when locked) - readOnly guards on node-actions and collection actions - Upload store for scan/guide file upload handling Viewer state: - previewSelectedIds for box-select live preview - hoverHighlightMode (default/delete) for delete-mode hover outline
91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
import { create } from 'zustand'
|
|
|
|
export type UploadStatus = 'preparing' | 'uploading' | 'confirming' | 'done' | 'error'
|
|
|
|
export interface UploadEntry {
|
|
status: UploadStatus
|
|
assetType: 'scan' | 'guide'
|
|
fileName: string
|
|
progress: number // 0-100
|
|
error: string | null
|
|
resultUrl: string | null
|
|
}
|
|
|
|
export type UploadHandler = (
|
|
projectId: string,
|
|
levelId: string,
|
|
file: File,
|
|
type: 'scan' | 'guide',
|
|
) => void
|
|
|
|
interface UploadState {
|
|
uploads: Record<string, UploadEntry>
|
|
uploadHandler: UploadHandler | null
|
|
registerUploadHandler: (handler: UploadHandler) => void
|
|
unregisterUploadHandler: () => void
|
|
startUpload: (levelId: string, assetType: 'scan' | 'guide', fileName: string) => void
|
|
setProgress: (levelId: string, progress: number) => void
|
|
setStatus: (levelId: string, status: UploadStatus) => void
|
|
setError: (levelId: string, error: string) => void
|
|
setResult: (levelId: string, url: string) => void
|
|
clearUpload: (levelId: string) => void
|
|
}
|
|
|
|
export const useUploadStore = create<UploadState>((set) => ({
|
|
uploads: {},
|
|
uploadHandler: null,
|
|
registerUploadHandler: (handler) => set({ uploadHandler: handler }),
|
|
unregisterUploadHandler: () => set({ uploadHandler: null }),
|
|
|
|
startUpload: (levelId, assetType, fileName) =>
|
|
set((s) => ({
|
|
uploads: {
|
|
...s.uploads,
|
|
[levelId]: {
|
|
status: 'preparing',
|
|
assetType,
|
|
fileName,
|
|
progress: 0,
|
|
error: null,
|
|
resultUrl: null,
|
|
},
|
|
},
|
|
})),
|
|
|
|
setProgress: (levelId, progress) =>
|
|
set((s) => {
|
|
const entry = s.uploads[levelId]
|
|
if (!entry) return s
|
|
return { uploads: { ...s.uploads, [levelId]: { ...entry, progress } } }
|
|
}),
|
|
|
|
setStatus: (levelId, status) =>
|
|
set((s) => {
|
|
const entry = s.uploads[levelId]
|
|
if (!entry) return s
|
|
return { uploads: { ...s.uploads, [levelId]: { ...entry, status } } }
|
|
}),
|
|
|
|
setError: (levelId, error) =>
|
|
set((s) => {
|
|
const entry = s.uploads[levelId]
|
|
if (!entry) return s
|
|
return { uploads: { ...s.uploads, [levelId]: { ...entry, status: 'error' as const, error } } }
|
|
}),
|
|
|
|
setResult: (levelId, url) =>
|
|
set((s) => {
|
|
const entry = s.uploads[levelId]
|
|
if (!entry) return s
|
|
return {
|
|
uploads: { ...s.uploads, [levelId]: { ...entry, status: 'done' as const, resultUrl: url } },
|
|
}
|
|
}),
|
|
|
|
clearUpload: (levelId) =>
|
|
set((s) => {
|
|
const { [levelId]: _, ...rest } = s.uploads
|
|
return { uploads: rest }
|
|
}),
|
|
}))
|