feat: editor improvements and collapsible sidebars

Add collapse/expand toggle for the AppShell navigation sidebar and the
editor properties panel (both persisted to localStorage). Bundles other
in-progress editor work including position anchors, outlet sizing, PBR
textures, window slope/frame depth, curtain metadata, and various 2D/3D
rendering tweaks.
This commit is contained in:
2026-04-08 12:27:57 +03:00
parent aa8a874348
commit d8a914bf2a
116 changed files with 7324 additions and 1114 deletions
@@ -76,7 +76,9 @@ function createInitialState(room: RoomFull): EditorState {
layerVisibility: { walls: true, electrical: true, furniture: true, measurements: true, annotations: true },
selectedElectricalIndex: null,
selectedFurnitureIndex: null,
annotations: [],
annotations: room.annotations ?? [],
furnitureProjectionIds: new Set(),
globalFurnitureOpacity: 1,
};
}
@@ -93,6 +95,7 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState {
openings: existingMatch ? room.openings : [],
electricalItems: room.electricalItems,
furnitureItems: room.furnitureItems,
annotations: room.annotations ?? state.annotations,
};
}
case 'UPDATE_ROOM_PROPS':
@@ -177,6 +180,8 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState {
return { ...state, zoom: action.zoom };
case 'SET_PAN_OFFSET':
return { ...state, panOffset: action.offset };
case 'SET_VIEW':
return { ...state, zoom: action.zoom, panOffset: action.offset };
case 'SET_GRID_SIZE':
return { ...state, gridSize: action.gridSize };
case 'TOGGLE_GRID':
@@ -251,18 +256,43 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState {
annotations: state.annotations.filter((a) => a.id !== action.id),
selectedIds: removeFromSet(state.selectedIds, action.id),
};
case 'TOGGLE_FURNITURE_PROJECTION': {
const next = new Set(state.furnitureProjectionIds);
if (next.has(action.id)) next.delete(action.id);
else next.add(action.id);
return { ...state, furnitureProjectionIds: next };
}
case 'SET_GLOBAL_FURNITURE_OPACITY': {
const clamped = Math.min(1, Math.max(0, action.opacity));
return { ...state, globalFurnitureOpacity: clamped };
}
// ── Import ──
case 'SYNC_SAVE': {
// Build set of all new IDs to prune stale selections
// Build set of all new IDs so we can prune any selection that did not survive
const newIds = new Set<string>();
for (const w of action.walls) newIds.add(w.id);
for (const o of action.openings) newIds.add(o.id);
for (const e of action.electricalItems) newIds.add(e.id);
for (const f of action.furnitureItems) newIds.add(f.id);
// Keep only selected IDs that still exist in the new data
const prunedSelection = new Set<string>();
// Remap selected IDs through the id map (so freshly created server items
// stay selected). Fall back to the original id when no mapping is given.
const idMap = action.idMap;
const remappedSelection = new Set<string>();
for (const id of state.selectedIds) {
if (newIds.has(id)) prunedSelection.add(id);
const next = idMap?.get(id) ?? id;
if (newIds.has(next)) remappedSelection.add(next);
}
// Use server annotations when provided; otherwise just remap attached ids
// for the existing client-only annotation list.
let remappedAnnotations = action.annotations
? [...action.annotations]
: state.annotations;
if (idMap) {
remappedAnnotations = remappedAnnotations.map((a) =>
a.attachedToId && idMap.has(a.attachedToId)
? { ...a, attachedToId: idMap.get(a.attachedToId)! }
: a,
);
}
return {
...state,
@@ -270,7 +300,8 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState {
openings: action.openings,
electricalItems: action.electricalItems,
furnitureItems: action.furnitureItems,
selectedIds: prunedSelection,
selectedIds: remappedSelection,
annotations: remappedAnnotations,
};
}
case 'IMPORT_ROOM':
@@ -445,6 +476,8 @@ interface SceneDataContextValue {
readonly electricalItems: readonly ElectricalItem[];
readonly furnitureItems: readonly FurnitureItem[];
readonly annotations: readonly Annotation[];
readonly furnitureProjectionIds: ReadonlySet<string>;
readonly globalFurnitureOpacity: number;
readonly gridSize: number;
readonly gridVisible: boolean;
readonly snapEnabled: boolean;
@@ -467,6 +500,7 @@ interface SceneDataContextValue {
addAnnotation(annotation: Annotation): void;
updateAnnotation(annotation: Annotation): void;
removeAnnotation(id: string): void;
toggleFurnitureProjection(id: string): void;
copySelected(): void;
pasteClipboard(): void;
}
@@ -499,6 +533,7 @@ interface EditorContextValue {
addAnnotation(annotation: Annotation): void;
updateAnnotation(annotation: Annotation): void;
removeAnnotation(id: string): void;
toggleFurnitureProjection(id: string): void;
copySelected(): void;
pasteClipboard(): void;
}
@@ -615,6 +650,10 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
(id: string) => dispatch({ type: 'REMOVE_ANNOTATION', id }),
[],
);
const toggleFurnitureProjection = useCallback(
(id: string) => dispatch({ type: 'TOGGLE_FURNITURE_PROJECTION', id }),
[],
);
// ── Clipboard (ref-based so copy reads current state without closures) ──
const clipboardRef = useRef<{
@@ -712,6 +751,8 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
electricalItems: state.electricalItems,
furnitureItems: state.furnitureItems,
annotations: state.annotations,
furnitureProjectionIds: state.furnitureProjectionIds,
globalFurnitureOpacity: state.globalFurnitureOpacity,
gridSize: state.gridSize,
gridVisible: state.gridVisible,
snapEnabled: state.snapEnabled,
@@ -734,6 +775,7 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
addAnnotation,
updateAnnotation,
removeAnnotation,
toggleFurnitureProjection,
copySelected,
pasteClipboard,
}),
@@ -744,6 +786,8 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
state.electricalItems,
state.furnitureItems,
state.annotations,
state.furnitureProjectionIds,
state.globalFurnitureOpacity,
state.gridSize,
state.gridVisible,
state.snapEnabled,
@@ -765,6 +809,7 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
addAnnotation,
updateAnnotation,
removeAnnotation,
toggleFurnitureProjection,
copySelected,
pasteClipboard,
],
@@ -796,6 +841,7 @@ export function EditorProvider({ room, children }: EditorProviderProps) {
addAnnotation,
updateAnnotation,
removeAnnotation,
toggleFurnitureProjection,
copySelected,
pasteClipboard,
}),