Pascal 0bd0cab533
feat: stair system, scene graph utilities, read-only mode, viewer state improvements (#210)
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
2026-04-05 01:20:43 -04:00

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 }
}),
}))