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:
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user