From d8a914bf2a469bb9aebe7b17cef0f9902fc1bdf7 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Wed, 8 Apr 2026 12:27:57 +0300 Subject: [PATCH] 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. --- .../client/public/locales/en/translation.json | 56 + .../client/public/locales/ru/translation.json | 56 + .../public/textures/floors/concrete/color.jpg | Bin 0 -> 26106 bytes .../textures/floors/concrete/normal.jpg | Bin 0 -> 40944 bytes .../textures/floors/concrete/roughness.jpg | Bin 0 -> 26541 bytes .../public/textures/floors/laminate/color.jpg | Bin 0 -> 43798 bytes .../textures/floors/laminate/normal.jpg | Bin 0 -> 14080 bytes .../textures/floors/laminate/roughness.jpg | Bin 0 -> 27874 bytes .../textures/floors/tile_gray/color.jpg | Bin 0 -> 115731 bytes .../textures/floors/tile_gray/normal.jpg | Bin 0 -> 27172 bytes .../textures/floors/tile_gray/roughness.jpg | Bin 0 -> 50762 bytes .../textures/floors/tile_white/color.jpg | Bin 0 -> 42398 bytes .../textures/floors/tile_white/normal.jpg | Bin 0 -> 2884 bytes .../textures/floors/tile_white/roughness.jpg | Bin 0 -> 28497 bytes .../textures/floors/wood_dark/color.jpg | Bin 0 -> 35922 bytes .../textures/floors/wood_dark/normal.jpg | Bin 0 -> 7053 bytes .../textures/floors/wood_dark/roughness.jpg | Bin 0 -> 31196 bytes .../floors/wood_herringbone/color.jpg | Bin 0 -> 47976 bytes .../floors/wood_herringbone/normal.jpg | Bin 0 -> 5374 bytes .../floors/wood_herringbone/roughness.jpg | Bin 0 -> 52920 bytes .../textures/floors/wood_light/color.jpg | Bin 0 -> 47974 bytes .../textures/floors/wood_light/normal.jpg | Bin 0 -> 14080 bytes .../textures/floors/wood_light/roughness.jpg | Bin 0 -> 27871 bytes .../textures/floors/wood_medium/color.jpg | Bin 0 -> 50460 bytes .../textures/floors/wood_medium/normal.jpg | Bin 0 -> 11366 bytes .../textures/floors/wood_medium/roughness.jpg | Bin 0 -> 18935 bytes .../public/textures/walls/brick/color.jpg | Bin 0 -> 68545 bytes .../public/textures/walls/brick/normal.jpg | Bin 0 -> 86693 bytes .../public/textures/walls/brick/roughness.jpg | Bin 0 -> 54827 bytes .../public/textures/walls/concrete/color.jpg | Bin 0 -> 30182 bytes .../public/textures/walls/concrete/normal.jpg | Bin 0 -> 33178 bytes .../textures/walls/concrete/roughness.jpg | Bin 0 -> 54626 bytes .../public/textures/walls/plaster/color.jpg | Bin 0 -> 19298 bytes .../public/textures/walls/plaster/normal.jpg | Bin 0 -> 41600 bytes .../textures/walls/plaster/roughness.jpg | Bin 0 -> 19713 bytes .../public/textures/walls/wallpaper/color.jpg | Bin 0 -> 61090 bytes .../textures/walls/wallpaper/normal.jpg | Bin 0 -> 101674 bytes .../textures/walls/wallpaper/roughness.jpg | Bin 0 -> 92466 bytes .../textures/walls/wood_panel/color.jpg | Bin 0 -> 23626 bytes .../textures/walls/wood_panel/normal.jpg | Bin 0 -> 59529 bytes .../textures/walls/wood_panel/roughness.jpg | Bin 0 -> 29948 bytes apps/client/src/api/client.ts | 23 + .../src/components/editor/EditorCanvas.tsx | 214 +-- .../src/components/editor/EditorToolbar.tsx | 33 +- .../src/components/editor/PropertiesPanel.tsx | 811 +++++++++++- .../components/editor/RoomEditorLayout.tsx | 291 +++- .../editor/context/EditorContext.tsx | 58 +- .../components/editor/export/roomFormat.ts | 18 + .../editor/layers/AnnotationLayer.tsx | 49 +- .../editor/layers/ElectricalLayer.tsx | 94 +- .../editor/layers/FurnitureLayer.tsx | 65 +- .../components/editor/layers/GridLayer.tsx | 6 +- .../editor/layers/MeasureOverlayLayer.tsx | 8 +- .../editor/layers/MeasurementLayer.tsx | 63 +- .../components/editor/layers/OpeningLayer.tsx | 19 +- .../editor/layers/RoomLabelLayer.tsx | 54 +- .../editor/layers/SelectionLayer.tsx | 6 +- .../components/editor/layers/WallLayer.tsx | 6 +- .../editor/panels/ElectricalPalette.tsx | 243 +++- .../editor/panels/FurniturePalette.tsx | 203 ++- .../editor/panels/item-picker.module.css | 220 +++ .../projection/ProjectionElectrical.tsx | 125 +- .../editor/projection/ProjectionFurniture.tsx | 5 +- .../projection/ProjectionMeasurements.tsx | 280 +++- .../editor/projection/ProjectionPanel.tsx | 58 +- .../editor/projection/ProjectionWindow.tsx | 81 +- .../editor/projection/WallProjectionView.tsx | 360 ++++- .../editor/properties-panel.module.css | 38 + .../symbols/electrical/OutletSymbol.tsx | 145 +- .../editor/symbols/electrical/index.ts | 8 +- .../editor/symbols/furniture/index.ts | 160 ++- .../editor/three/CameraControls.tsx | 171 ++- .../components/editor/three/DoorOpening.tsx | 34 +- .../editor/three/ElectricalMesh.tsx | 117 +- .../components/editor/three/FloorCeiling.tsx | 197 +-- .../components/editor/three/FurnitureMesh.tsx | 1175 ++++++++++++++++- .../components/editor/three/PlinthMesh.tsx | 23 +- .../components/editor/three/Room3DView.tsx | 282 +++- .../src/components/editor/three/WallMesh.tsx | 130 +- .../components/editor/three/WindowOpening.tsx | 117 +- .../editor/three/camera-view-cube.module.css | 179 +++ .../editor/three/utils/pbrTextures.ts | 124 ++ .../editor/three/utils/wallGeometry.ts | 25 +- .../src/components/editor/tools/DoorTool.ts | 15 + .../components/editor/tools/ElectricalTool.ts | 12 +- .../components/editor/tools/FurnitureTool.ts | 34 +- .../src/components/editor/tools/SelectTool.ts | 49 +- .../src/components/editor/tools/WindowTool.ts | 9 + apps/client/src/components/editor/types.ts | 16 +- .../__tests__/collisionDetection.test.ts | 1 + .../editor/utils/__tests__/wallUtils.test.ts | 10 + .../src/components/editor/utils/angle.ts | 9 + .../editor/utils/collisionDetection.ts | 128 +- .../editor/utils/curtainMetadata.ts | 69 + .../editor/utils/projectionMapping.ts | 208 ++- .../client/src/components/layout/AppShell.tsx | 52 +- .../components/layout/app-shell.module.css | 48 + apps/client/src/components/rooms/RoomCard.tsx | 22 +- .../src/components/ui/TextPromptModal.tsx | 108 ++ .../ui/text-prompt-modal.module.css | 77 ++ apps/client/src/main.tsx | 18 + apps/client/src/pages/ApartmentDetailPage.tsx | 31 + apps/client/src/styles/global.css | 29 +- .../migration.sql | 109 ++ .../migration.sql | 3 + .../migration.sql | 4 + apps/server/prisma/schema.prisma | 35 + apps/server/src/routes/elements.ts | 142 ++ apps/server/src/routes/rooms.ts | 188 +++ apps/server/src/utils/mappers.ts | 71 + packages/shared/src/index.ts | 20 +- .../shared/src/schemas/elements.schema.ts | 71 +- packages/shared/src/schemas/room.schema.ts | 9 +- packages/shared/src/types/elements.ts | 240 ++++ packages/shared/src/types/room.ts | 37 +- scripts/fetch-textures.py | 134 ++ 116 files changed, 7324 insertions(+), 1114 deletions(-) create mode 100644 apps/client/public/textures/floors/concrete/color.jpg create mode 100644 apps/client/public/textures/floors/concrete/normal.jpg create mode 100644 apps/client/public/textures/floors/concrete/roughness.jpg create mode 100644 apps/client/public/textures/floors/laminate/color.jpg create mode 100644 apps/client/public/textures/floors/laminate/normal.jpg create mode 100644 apps/client/public/textures/floors/laminate/roughness.jpg create mode 100644 apps/client/public/textures/floors/tile_gray/color.jpg create mode 100644 apps/client/public/textures/floors/tile_gray/normal.jpg create mode 100644 apps/client/public/textures/floors/tile_gray/roughness.jpg create mode 100644 apps/client/public/textures/floors/tile_white/color.jpg create mode 100644 apps/client/public/textures/floors/tile_white/normal.jpg create mode 100644 apps/client/public/textures/floors/tile_white/roughness.jpg create mode 100644 apps/client/public/textures/floors/wood_dark/color.jpg create mode 100644 apps/client/public/textures/floors/wood_dark/normal.jpg create mode 100644 apps/client/public/textures/floors/wood_dark/roughness.jpg create mode 100644 apps/client/public/textures/floors/wood_herringbone/color.jpg create mode 100644 apps/client/public/textures/floors/wood_herringbone/normal.jpg create mode 100644 apps/client/public/textures/floors/wood_herringbone/roughness.jpg create mode 100644 apps/client/public/textures/floors/wood_light/color.jpg create mode 100644 apps/client/public/textures/floors/wood_light/normal.jpg create mode 100644 apps/client/public/textures/floors/wood_light/roughness.jpg create mode 100644 apps/client/public/textures/floors/wood_medium/color.jpg create mode 100644 apps/client/public/textures/floors/wood_medium/normal.jpg create mode 100644 apps/client/public/textures/floors/wood_medium/roughness.jpg create mode 100644 apps/client/public/textures/walls/brick/color.jpg create mode 100644 apps/client/public/textures/walls/brick/normal.jpg create mode 100644 apps/client/public/textures/walls/brick/roughness.jpg create mode 100644 apps/client/public/textures/walls/concrete/color.jpg create mode 100644 apps/client/public/textures/walls/concrete/normal.jpg create mode 100644 apps/client/public/textures/walls/concrete/roughness.jpg create mode 100644 apps/client/public/textures/walls/plaster/color.jpg create mode 100644 apps/client/public/textures/walls/plaster/normal.jpg create mode 100644 apps/client/public/textures/walls/plaster/roughness.jpg create mode 100644 apps/client/public/textures/walls/wallpaper/color.jpg create mode 100644 apps/client/public/textures/walls/wallpaper/normal.jpg create mode 100644 apps/client/public/textures/walls/wallpaper/roughness.jpg create mode 100644 apps/client/public/textures/walls/wood_panel/color.jpg create mode 100644 apps/client/public/textures/walls/wood_panel/normal.jpg create mode 100644 apps/client/public/textures/walls/wood_panel/roughness.jpg create mode 100644 apps/client/src/components/editor/panels/item-picker.module.css create mode 100644 apps/client/src/components/editor/three/camera-view-cube.module.css create mode 100644 apps/client/src/components/editor/three/utils/pbrTextures.ts create mode 100644 apps/client/src/components/editor/utils/angle.ts create mode 100644 apps/client/src/components/editor/utils/curtainMetadata.ts create mode 100644 apps/client/src/components/ui/TextPromptModal.tsx create mode 100644 apps/client/src/components/ui/text-prompt-modal.module.css create mode 100644 apps/server/prisma/migrations/20260407100549_add_anchors_outlet_count_and_room_outlet_size/migration.sql create mode 100644 apps/server/prisma/migrations/20260408030305_add_window_slope_depth/migration.sql create mode 100644 apps/server/prisma/migrations/20260408032825_add_opening_frame_thickness/migration.sql create mode 100644 scripts/fetch-textures.py diff --git a/apps/client/public/locales/en/translation.json b/apps/client/public/locales/en/translation.json index ea6a7b1..ec8bd5e 100644 --- a/apps/client/public/locales/en/translation.json +++ b/apps/client/public/locales/en/translation.json @@ -22,6 +22,8 @@ "furniture.other": "Other", "nav.apartments": "Apartments", + "nav.collapse": "Collapse sidebar", + "nav.expand": "Expand sidebar", "breadcrumb.apartments": "Apartments", "breadcrumb.apartmentDetails": "Apartment Details", @@ -108,6 +110,13 @@ "roomCard.edit": "Edit", "roomCard.delete": "Delete", + "roomCard.clone": "Clone", + "view3d.lightControls": "Light", + "view3d.azimuth": "Azimuth", + "view3d.elevation": "Elevation", + "view3d.intensity": "Intensity", + "view3d.reset": "Reset", + "view3d.doorsOpen": "Show doors open", "common.cancel": "Cancel", "common.delete": "Delete", @@ -162,6 +171,8 @@ "toolbar.distributeV": "Distribute vertical", "properties.title": "Properties", + "properties.collapse": "Collapse panel", + "properties.expand": "Expand panel", "properties.area": "Area", "properties.perimeter": "Perimeter", "properties.noSelection": "No element selected", @@ -197,6 +208,14 @@ "properties.yes": "Yes", "properties.depth": "Depth", "properties.wallColor": "Wall color", + "properties.wallFinish": "Wall finish", + "properties.wallColorPaintOnly": "Wall color only applies to the Paint finish", + "wallFinish.PAINT": "Paint", + "wallFinish.PLASTER": "Plaster", + "wallFinish.BRICK": "Brick", + "wallFinish.CONCRETE": "Concrete", + "wallFinish.WOOD_PANEL": "Wood panel", + "wallFinish.WALLPAPER": "Wallpaper", "properties.floorType": "Floor", "floor.CONCRETE": "Concrete", "floor.WOOD_LIGHT": "Light Wood", @@ -207,6 +226,31 @@ "floor.TILE_GRAY": "Gray Tile", "floor.LAMINATE": "Laminate", "properties.addNote": "Add note", + "properties.showProjection": "Show on wall projection", + "properties.opacity": "Opacity", + "properties.customLabel": "Title", + "properties.windowGridCols": "Grid columns", + "properties.windowGridRows": "Grid rows", + "properties.windowSlopeDepth": "Reveal depth", + "properties.openingFrameThickness": "Frame thickness", + "properties.shelfRows": "Shelf rows", + "properties.hasBackPanel": "Back panel", + "properties.curtainOpen": "Open", + "properties.curtainLeftOpen": "Left open", + "properties.curtainRightOpen": "Right open", + "properties.curtainFabricColor": "Fabric color", + "properties.outletWidth": "Outlet width", + "properties.outletHeight": "Outlet height", + "properties.outletCount": "Count", + "properties.anchor": "Anchor", + "anchor.left": "Left", + "anchor.middle": "Middle", + "anchor.right": "Right", + "anchor.top": "Top", + "anchor.bottom": "Bottom", + "toolbar.furnitureOpacity": "Furniture opacity", + "annotation.edit": "Edit", + "annotation.delete": "Delete", "properties.stand": "Stand", "properties.openDirection": "Open direction", "properties.openDir.LEFT": "Left", @@ -226,6 +270,18 @@ "electrical.cable": "Cable", "furniture.title": "Furniture", + "furniture.searchPlaceholder": "Search furniture\u2026", + "furniture.noResults": "No matching furniture", + "electrical.searchPlaceholder": "Search electrical\u2026", + "electrical.noResults": "No matching items", + "furnitureCategory.all": "All", + "furnitureCategory.sleeping": "Sleeping", + "furnitureCategory.seating": "Seating", + "furnitureCategory.tables": "Tables", + "furnitureCategory.storage": "Storage", + "furnitureCategory.electronics": "Electronics", + "furnitureCategory.climate": "Climate", + "furnitureCategory.decor": "Decor", "cableLength.label": "Cable length:", diff --git a/apps/client/public/locales/ru/translation.json b/apps/client/public/locales/ru/translation.json index 5cf23a3..c0ebbc2 100644 --- a/apps/client/public/locales/ru/translation.json +++ b/apps/client/public/locales/ru/translation.json @@ -22,6 +22,8 @@ "furniture.other": "Другое", "nav.apartments": "Квартиры", + "nav.collapse": "Свернуть боковую панель", + "nav.expand": "Развернуть боковую панель", "breadcrumb.apartments": "Квартиры", "breadcrumb.apartmentDetails": "Детали квартиры", @@ -111,6 +113,13 @@ "roomCard.edit": "Изменить", "roomCard.delete": "Удалить", + "roomCard.clone": "Дублировать", + "view3d.lightControls": "Свет", + "view3d.azimuth": "Азимут", + "view3d.elevation": "Высота", + "view3d.intensity": "Интенсивность", + "view3d.reset": "Сброс", + "view3d.doorsOpen": "Показать двери открытыми", "common.cancel": "Отмена", "common.delete": "Удалить", @@ -165,6 +174,8 @@ "toolbar.distributeV": "Распределить по вертикали", "properties.title": "Свойства", + "properties.collapse": "Свернуть панель", + "properties.expand": "Развернуть панель", "properties.area": "Площадь", "properties.perimeter": "Периметр", "properties.noSelection": "Элемент не выбран", @@ -200,6 +211,14 @@ "properties.yes": "Да", "properties.depth": "Глубина", "properties.wallColor": "Цвет стен", + "properties.wallFinish": "Отделка стен", + "properties.wallColorPaintOnly": "Цвет применяется только к покраске", + "wallFinish.PAINT": "Покраска", + "wallFinish.PLASTER": "Штукатурка", + "wallFinish.BRICK": "Кирпич", + "wallFinish.CONCRETE": "Бетон", + "wallFinish.WOOD_PANEL": "Деревянная панель", + "wallFinish.WALLPAPER": "Обои", "properties.floorType": "Пол", "floor.CONCRETE": "Бетон", "floor.WOOD_LIGHT": "Светлое дерево", @@ -210,6 +229,31 @@ "floor.TILE_GRAY": "Серая плитка", "floor.LAMINATE": "Ламинат", "properties.addNote": "Добавить заметку", + "properties.showProjection": "Показать на проекции стены", + "properties.opacity": "Прозрачность", + "properties.customLabel": "Название", + "properties.windowGridCols": "Сетка: столбцы", + "properties.windowGridRows": "Сетка: строки", + "properties.windowSlopeDepth": "Глубина откоса", + "properties.openingFrameThickness": "Толщина рамы", + "properties.shelfRows": "Количество полок", + "properties.hasBackPanel": "Задняя стенка", + "properties.curtainOpen": "Раскрытие", + "properties.curtainLeftOpen": "Левая створка", + "properties.curtainRightOpen": "Правая створка", + "properties.curtainFabricColor": "Цвет ткани", + "properties.outletWidth": "Ширина розетки", + "properties.outletHeight": "Высота розетки", + "properties.outletCount": "Количество", + "properties.anchor": "Привязка", + "anchor.left": "Слева", + "anchor.middle": "По центру", + "anchor.right": "Справа", + "anchor.top": "Сверху", + "anchor.bottom": "Снизу", + "toolbar.furnitureOpacity": "Прозрачность мебели", + "annotation.edit": "Изменить", + "annotation.delete": "Удалить", "properties.stand": "Подставка", "properties.openDirection": "Направление открытия", "properties.openDir.LEFT": "Влево", @@ -229,6 +273,18 @@ "electrical.cable": "Кабель", "furniture.title": "Мебель", + "furniture.searchPlaceholder": "Поиск мебели\u2026", + "furniture.noResults": "Ничего не найдено", + "electrical.searchPlaceholder": "Поиск элементов\u2026", + "electrical.noResults": "Ничего не найдено", + "furnitureCategory.all": "Все", + "furnitureCategory.sleeping": "Спальня", + "furnitureCategory.seating": "Сиденья", + "furnitureCategory.tables": "Столы", + "furnitureCategory.storage": "Хранение", + "furnitureCategory.electronics": "Электроника", + "furnitureCategory.climate": "Климат", + "furnitureCategory.decor": "Декор", "cableLength.label": "Длина кабеля:", diff --git a/apps/client/public/textures/floors/concrete/color.jpg b/apps/client/public/textures/floors/concrete/color.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bffa2c2d7bb4aed403f0d9f8a6bacc8240578450 GIT binary patch literal 26106 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