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:
@@ -19,13 +19,14 @@ export type {
|
||||
export type {
|
||||
Point,
|
||||
FloorType,
|
||||
WallFinish,
|
||||
Room,
|
||||
RoomFull,
|
||||
CreateRoomDto,
|
||||
UpdateRoomDto,
|
||||
} from './types/room.js';
|
||||
|
||||
export { FLOOR_TYPES } from './types/room.js';
|
||||
export { FLOOR_TYPES, WALL_FINISHES, DEFAULT_OUTLET_WIDTH, DEFAULT_OUTLET_HEIGHT } from './types/room.js';
|
||||
|
||||
export type {
|
||||
WallDirection,
|
||||
@@ -36,6 +37,9 @@ export type {
|
||||
WallOpening,
|
||||
CreateWallOpeningDto,
|
||||
UpdateWallOpeningDto,
|
||||
HorizontalAnchor,
|
||||
VerticalAnchor,
|
||||
PositionAnchor,
|
||||
ElectricalType,
|
||||
ElectricalItem,
|
||||
CreateElectricalItemDto,
|
||||
@@ -45,9 +49,12 @@ export type {
|
||||
CreateFurnitureItemDto,
|
||||
UpdateFurnitureItemDto,
|
||||
Annotation,
|
||||
CreateAnnotationDto,
|
||||
UpdateAnnotationDto,
|
||||
BatchSyncOpeningsDto,
|
||||
BatchSyncElectricalDto,
|
||||
BatchSyncFurnitureDto,
|
||||
BatchSyncAnnotationsDto,
|
||||
} from './types/elements.js';
|
||||
|
||||
export {
|
||||
@@ -56,6 +63,11 @@ export {
|
||||
DOOR_OPEN_DIRECTIONS,
|
||||
ELECTRICAL_TYPES,
|
||||
FURNITURE_TYPES,
|
||||
HORIZONTAL_ANCHORS,
|
||||
VERTICAL_ANCHORS,
|
||||
DEFAULT_POSITION_ANCHOR,
|
||||
anchorOffsetToCenter,
|
||||
rotatedAnchorOffsetToCenter,
|
||||
} from './types/elements.js';
|
||||
|
||||
export type {
|
||||
@@ -90,6 +102,9 @@ export {
|
||||
batchSyncOpeningsSchema,
|
||||
batchSyncElectricalSchema,
|
||||
batchSyncFurnitureSchema,
|
||||
createAnnotationSchema,
|
||||
updateAnnotationSchema,
|
||||
batchSyncAnnotationsSchema,
|
||||
type BulkUpdateWallsInput,
|
||||
type CreateWallOpeningInput,
|
||||
type UpdateWallOpeningInput,
|
||||
@@ -100,4 +115,7 @@ export {
|
||||
type BatchSyncOpeningsInput,
|
||||
type BatchSyncElectricalInput,
|
||||
type BatchSyncFurnitureInput,
|
||||
type CreateAnnotationInput,
|
||||
type UpdateAnnotationInput,
|
||||
type BatchSyncAnnotationsInput,
|
||||
} from './schemas/elements.schema.js';
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
import { WALL_DIRECTIONS, OPENING_TYPES, DOOR_OPEN_DIRECTIONS, ELECTRICAL_TYPES, FURNITURE_TYPES } from '../types/elements.js';
|
||||
import {
|
||||
WALL_DIRECTIONS,
|
||||
OPENING_TYPES,
|
||||
DOOR_OPEN_DIRECTIONS,
|
||||
ELECTRICAL_TYPES,
|
||||
FURNITURE_TYPES,
|
||||
HORIZONTAL_ANCHORS,
|
||||
VERTICAL_ANCHORS,
|
||||
} from '../types/elements.js';
|
||||
|
||||
// ── Position anchor ──
|
||||
|
||||
const positionAnchorSchema = z.object({
|
||||
horizontal: z.enum(HORIZONTAL_ANCHORS),
|
||||
vertical: z.enum(VERTICAL_ANCHORS),
|
||||
});
|
||||
|
||||
// ── Wall schemas ──
|
||||
|
||||
@@ -34,6 +49,11 @@ export const createWallOpeningSchema = z.object({
|
||||
height: z.number().positive('Height must be positive'),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
openDirection: doorOpenDirectionEnum.optional(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
gridCols: z.number().int().min(1).max(10).optional(),
|
||||
gridRows: z.number().int().min(1).max(10).optional(),
|
||||
slopeDepth: z.number().min(0).max(2).optional(),
|
||||
frameThickness: z.number().min(0).max(0.5).optional(),
|
||||
});
|
||||
|
||||
export const updateWallOpeningSchema = z.object({
|
||||
@@ -43,6 +63,11 @@ export const updateWallOpeningSchema = z.object({
|
||||
height: z.number().positive('Height must be positive').optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
openDirection: doorOpenDirectionEnum.optional(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
gridCols: z.number().int().min(1).max(10).optional(),
|
||||
gridRows: z.number().int().min(1).max(10).optional(),
|
||||
slopeDepth: z.number().min(0).max(2).optional(),
|
||||
frameThickness: z.number().min(0).max(0.5).optional(),
|
||||
});
|
||||
|
||||
export type CreateWallOpeningInput = z.infer<typeof createWallOpeningSchema>;
|
||||
@@ -59,6 +84,9 @@ export const createElectricalItemSchema = z.object({
|
||||
wallId: z.string().nullish(),
|
||||
elevationFromFloor: z.number().min(0).nullish(),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
count: z.number().int().min(1).max(20).optional(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
@@ -69,6 +97,9 @@ export const updateElectricalItemSchema = z.object({
|
||||
wallId: z.string().nullish(),
|
||||
elevationFromFloor: z.number().min(0).nullish(),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
count: z.number().int().min(1).max(20).optional(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
@@ -89,6 +120,10 @@ export const createFurnitureItemSchema = z.object({
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
showProjection: z.boolean().optional(),
|
||||
opacity: z.number().min(0).max(1).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export const updateFurnitureItemSchema = z.object({
|
||||
@@ -101,6 +136,10 @@ export const updateFurnitureItemSchema = z.object({
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
positionAnchor: positionAnchorSchema.optional(),
|
||||
showProjection: z.boolean().optional(),
|
||||
opacity: z.number().min(0).max(1).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export type CreateFurnitureItemInput = z.infer<typeof createFurnitureItemSchema>;
|
||||
@@ -146,3 +185,33 @@ export const batchSyncFurnitureSchema = z.object({
|
||||
});
|
||||
|
||||
export type BatchSyncFurnitureInput = z.infer<typeof batchSyncFurnitureSchema>;
|
||||
|
||||
// ── Annotation schemas ──
|
||||
|
||||
export const createAnnotationSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
text: z.string().max(2000),
|
||||
fontSize: z.number().int().positive().max(96).optional(),
|
||||
color: z.string().max(32).optional(),
|
||||
attachedToId: z.string().nullish(),
|
||||
projectionOffsetX: z.number().nullish(),
|
||||
projectionOffsetY: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const updateAnnotationSchema = createAnnotationSchema.partial();
|
||||
|
||||
export const batchSyncAnnotationsSchema = z.object({
|
||||
create: z.array(createAnnotationSchema),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
data: updateAnnotationSchema,
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string().min(1)),
|
||||
});
|
||||
|
||||
export type CreateAnnotationInput = z.infer<typeof createAnnotationSchema>;
|
||||
export type UpdateAnnotationInput = z.infer<typeof updateAnnotationSchema>;
|
||||
export type BatchSyncAnnotationsInput = z.infer<typeof batchSyncAnnotationsSchema>;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import { FLOOR_TYPES } from '../types/room.js';
|
||||
import { FLOOR_TYPES, WALL_FINISHES } from '../types/room.js';
|
||||
|
||||
const floorTypeEnum = z.enum(FLOOR_TYPES);
|
||||
const wallFinishEnum = z.enum(WALL_FINISHES);
|
||||
|
||||
const pointSchema = z.object({
|
||||
x: z.number(),
|
||||
@@ -21,6 +22,9 @@ export const createRoomSchema = z.object({
|
||||
posY: z.number().optional(),
|
||||
floorType: floorTypeEnum.optional(),
|
||||
wallColor: z.string().max(20).optional(),
|
||||
wallFinish: wallFinishEnum.optional(),
|
||||
outletWidth: z.number().positive('Outlet width must be positive').optional(),
|
||||
outletHeight: z.number().positive('Outlet height must be positive').optional(),
|
||||
});
|
||||
|
||||
export const updateRoomSchema = z.object({
|
||||
@@ -36,6 +40,9 @@ export const updateRoomSchema = z.object({
|
||||
posY: z.number().optional(),
|
||||
floorType: floorTypeEnum.optional(),
|
||||
wallColor: z.string().max(20).optional(),
|
||||
wallFinish: wallFinishEnum.optional(),
|
||||
outletWidth: z.number().positive('Outlet width must be positive').optional(),
|
||||
outletHeight: z.number().positive('Outlet height must be positive').optional(),
|
||||
});
|
||||
|
||||
export type CreateRoomInput = z.infer<typeof createRoomSchema>;
|
||||
|
||||
@@ -36,11 +36,45 @@ export interface WallOpening {
|
||||
readonly roomId: string;
|
||||
readonly wallId: string;
|
||||
readonly type: OpeningType;
|
||||
/**
|
||||
* Position along the wall (meters from wall start). Interpreted relative to
|
||||
* `positionAnchor.horizontal`: `left` = left edge, `middle` = center,
|
||||
* `right` = right edge of the opening's bounding box along the wall.
|
||||
*/
|
||||
readonly positionAlongWall: number;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
/**
|
||||
* Elevation from the floor (meters). Interpreted relative to
|
||||
* `positionAnchor.vertical`: `top` = top edge, `middle` = center,
|
||||
* `bottom` = bottom edge of the opening's bounding box.
|
||||
*/
|
||||
readonly elevationFromFloor: number;
|
||||
readonly openDirection: DoorOpenDirection;
|
||||
/** Defines what (positionAlongWall, elevationFromFloor) refers to on the opening's bounding box. */
|
||||
readonly positionAnchor: PositionAnchor;
|
||||
/**
|
||||
* Window grid subdivision. `gridCols` is the number of vertical panes
|
||||
* across the window (so N-1 internal mullions), `gridRows` is the
|
||||
* number of horizontal panes stacked vertically. Both default to 2 for
|
||||
* newly created windows (classic 2×2 cross). Ignored for doors.
|
||||
*/
|
||||
readonly gridCols: number;
|
||||
readonly gridRows: number;
|
||||
/**
|
||||
* Window reveal (Russian: откос) depth in meters. Defines the depth of
|
||||
* the angled jamb/sill panels framing the window inside the wall opening.
|
||||
* `0` means no visible reveal — the window appears flush with the wall
|
||||
* surface. Ignored for doors. Renderers should clamp the value so it does
|
||||
* not exceed half the wall thickness.
|
||||
*/
|
||||
readonly slopeDepth: number;
|
||||
/**
|
||||
* Frame member thickness in meters (used for both doors and windows).
|
||||
* Controls the visual width of the frame posts/bars in the 3D view.
|
||||
* Defaults to 0.03 m if unset.
|
||||
*/
|
||||
readonly frameThickness: number;
|
||||
}
|
||||
|
||||
export interface CreateWallOpeningDto {
|
||||
@@ -51,6 +85,11 @@ export interface CreateWallOpeningDto {
|
||||
readonly height: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly openDirection?: DoorOpenDirection;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly gridCols?: number;
|
||||
readonly gridRows?: number;
|
||||
readonly slopeDepth?: number;
|
||||
readonly frameThickness?: number;
|
||||
}
|
||||
|
||||
export interface UpdateWallOpeningDto {
|
||||
@@ -60,6 +99,117 @@ export interface UpdateWallOpeningDto {
|
||||
readonly height?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly openDirection?: DoorOpenDirection;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly gridCols?: number;
|
||||
readonly gridRows?: number;
|
||||
readonly slopeDepth?: number;
|
||||
readonly frameThickness?: number;
|
||||
}
|
||||
|
||||
// ── Position anchor ──
|
||||
//
|
||||
// Describes which point on an item's bounding box its (x, y) refers to.
|
||||
// Applies to electrical items and furniture. `middle/middle` is the default
|
||||
// for newly created items; legacy data may use other anchors (furniture
|
||||
// historically used `left/top` and electrical historically used `middle/middle`).
|
||||
|
||||
export const HORIZONTAL_ANCHORS = ['left', 'middle', 'right'] as const;
|
||||
export type HorizontalAnchor = (typeof HORIZONTAL_ANCHORS)[number];
|
||||
|
||||
export const VERTICAL_ANCHORS = ['top', 'middle', 'bottom'] as const;
|
||||
export type VerticalAnchor = (typeof VERTICAL_ANCHORS)[number];
|
||||
|
||||
export interface PositionAnchor {
|
||||
readonly horizontal: HorizontalAnchor;
|
||||
readonly vertical: VerticalAnchor;
|
||||
}
|
||||
|
||||
export const DEFAULT_POSITION_ANCHOR: PositionAnchor = {
|
||||
horizontal: 'middle',
|
||||
vertical: 'middle',
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the offset (in local item coordinates, before rotation) from the
|
||||
* anchored point to the bounding-box center, given the box dimensions.
|
||||
*
|
||||
* Convention: local +x = right, local +y = down (top-left origin).
|
||||
* Returns the vector that, when added to the anchored (x, y), yields the
|
||||
* geometric center of the bounding box.
|
||||
*
|
||||
* Note: this helper IGNORES rotation. For items where the anchor should
|
||||
* track the rotated visual (e.g. "left" should always mean the leftmost
|
||||
* edge of the rendered shape), use {@link rotatedAnchorOffsetToCenter}.
|
||||
*/
|
||||
export function anchorOffsetToCenter(
|
||||
anchor: PositionAnchor,
|
||||
width: number,
|
||||
height: number,
|
||||
): { readonly dx: number; readonly dy: number } {
|
||||
const dx =
|
||||
anchor.horizontal === 'left'
|
||||
? width / 2
|
||||
: anchor.horizontal === 'right'
|
||||
? -width / 2
|
||||
: 0;
|
||||
const dy =
|
||||
anchor.vertical === 'top'
|
||||
? height / 2
|
||||
: anchor.vertical === 'bottom'
|
||||
? -height / 2
|
||||
: 0;
|
||||
return { dx, dy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the offset from the anchored point to the bounding-box center,
|
||||
* **respecting rotation**. The horizontal/vertical anchor labels refer to
|
||||
* the rotated visual's world axes — e.g. with `anchor.horizontal = 'left'`
|
||||
* the (x, y) point is the leftmost edge of the rotated rectangle as seen
|
||||
* in world space, regardless of how the underlying width/depth are oriented
|
||||
* in the item's local frame.
|
||||
*
|
||||
* Math: a `width × height` rectangle rotated by `rotationDeg` produces a
|
||||
* world-axis-aligned bounding box with half-extents
|
||||
* `halfX = |w/2 · cos r| + |d/2 · sin r|`
|
||||
* `halfY = |w/2 · sin r| + |d/2 · cos r|`
|
||||
* For each anchor, the center sits at ±halfX or ±halfY from the anchored
|
||||
* point along the world axis.
|
||||
*
|
||||
* Reduces to {@link anchorOffsetToCenter} when `rotationDeg = 0`. For the
|
||||
* `middle/middle` anchor (the default for newly placed items) the offset is
|
||||
* always `(0, 0)` regardless of rotation.
|
||||
*/
|
||||
export function rotatedAnchorOffsetToCenter(
|
||||
anchor: PositionAnchor,
|
||||
width: number,
|
||||
height: number,
|
||||
rotationDeg: number,
|
||||
): { readonly dx: number; readonly dy: number } {
|
||||
const r = (rotationDeg * Math.PI) / 180;
|
||||
const cos = Math.cos(r);
|
||||
const sin = Math.sin(r);
|
||||
const halfW = width / 2;
|
||||
const halfH = height / 2;
|
||||
// World-axis half-extents of the rotated rectangle. Both terms get
|
||||
// absolute-valued so the result is the support along the +x / +y axes
|
||||
// regardless of rotation quadrant.
|
||||
const halfX = Math.abs(halfW * cos) + Math.abs(halfH * sin);
|
||||
const halfY = Math.abs(halfW * sin) + Math.abs(halfH * cos);
|
||||
|
||||
const dx =
|
||||
anchor.horizontal === 'left'
|
||||
? halfX
|
||||
: anchor.horizontal === 'right'
|
||||
? -halfX
|
||||
: 0;
|
||||
const dy =
|
||||
anchor.vertical === 'top'
|
||||
? halfY
|
||||
: anchor.vertical === 'bottom'
|
||||
? -halfY
|
||||
: 0;
|
||||
return { dx, dy };
|
||||
}
|
||||
|
||||
// ── ElectricalItem ──
|
||||
@@ -83,6 +233,21 @@ export interface ElectricalItem {
|
||||
readonly wallId: string | null;
|
||||
readonly elevationFromFloor: number | null;
|
||||
readonly rotation: number;
|
||||
/**
|
||||
* For OUTLET: number of individual outlets in the group (>= 1). Renderers
|
||||
* should draw `count` adjacent outlet boundaries sized by the room's
|
||||
* `outletWidth`/`outletHeight`. Ignored for other electrical types.
|
||||
*/
|
||||
readonly count: number;
|
||||
/** Defines what the (x, y) coordinates refer to on the bounding box. */
|
||||
readonly positionAnchor: PositionAnchor;
|
||||
/**
|
||||
* User-supplied display name for this item (e.g. "Behind TV", "Counter"),
|
||||
* used in the projection view and properties panel as an override for the
|
||||
* generic type code. `null` means "use the default label from the symbol
|
||||
* definition" — UI components should fall back to that, not write it back.
|
||||
*/
|
||||
readonly label: string | null;
|
||||
readonly metadata: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -93,6 +258,9 @@ export interface CreateElectricalItemDto {
|
||||
readonly wallId?: string | null;
|
||||
readonly elevationFromFloor?: number | null;
|
||||
readonly rotation?: number;
|
||||
readonly count?: number;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly label?: string | null;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -103,6 +271,9 @@ export interface UpdateElectricalItemDto {
|
||||
readonly wallId?: string | null;
|
||||
readonly elevationFromFloor?: number | null;
|
||||
readonly rotation?: number;
|
||||
readonly count?: number;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly label?: string | null;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -110,17 +281,28 @@ export interface UpdateElectricalItemDto {
|
||||
|
||||
export const FURNITURE_TYPES = [
|
||||
'BED',
|
||||
'CRIB',
|
||||
'DESK',
|
||||
'WARDROBE',
|
||||
'SOFA',
|
||||
'TABLE',
|
||||
'CHAIR',
|
||||
'OFFICE_CHAIR',
|
||||
'SHELF',
|
||||
'NIGHTSTAND',
|
||||
'DRESSER',
|
||||
'DRESSING_TABLE',
|
||||
'BOOKCASE',
|
||||
'TV',
|
||||
'PC_TOWER',
|
||||
'AC_UNIT',
|
||||
'RADIATOR',
|
||||
'WALL_COLLAGE',
|
||||
'CURTAIN',
|
||||
'PLANT',
|
||||
'MIRROR',
|
||||
'DIGITAL_PIANO',
|
||||
'SPEAKER',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type FurnitureType = (typeof FURNITURE_TYPES)[number];
|
||||
@@ -137,6 +319,19 @@ export interface FurnitureItem {
|
||||
readonly rotation: number;
|
||||
readonly elevationFromFloor: number;
|
||||
readonly label: string | null;
|
||||
/** Defines what the (x, y) coordinates refer to on the bounding box. */
|
||||
readonly positionAnchor: PositionAnchor;
|
||||
/** When true, the wall projection view shows dimension/offset overlays for this item. */
|
||||
readonly showProjection?: boolean;
|
||||
/** Render opacity in the [0, 1] range (1 = fully opaque). */
|
||||
readonly opacity?: number;
|
||||
/**
|
||||
* Type-specific extension bag for properties that don't deserve their own
|
||||
* column. Keys are domain-specific — e.g. CURTAIN uses `openAmount: number`
|
||||
* (0=closed, 1=fully open) and `fabricColor: string` (hex). Persisted as
|
||||
* JSON in the database.
|
||||
*/
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface CreateFurnitureItemDto {
|
||||
@@ -149,6 +344,10 @@ export interface CreateFurnitureItemDto {
|
||||
readonly rotation?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly label?: string | null;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly showProjection?: boolean;
|
||||
readonly opacity?: number;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface UpdateFurnitureItemDto {
|
||||
@@ -161,6 +360,10 @@ export interface UpdateFurnitureItemDto {
|
||||
readonly rotation?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly label?: string | null;
|
||||
readonly positionAnchor?: PositionAnchor;
|
||||
readonly showProjection?: boolean;
|
||||
readonly opacity?: number;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// ── Annotation ──
|
||||
@@ -175,6 +378,15 @@ export interface Annotation {
|
||||
readonly color?: string;
|
||||
/** If set, this annotation follows the item with this ID. x,y become offsets from the item. */
|
||||
readonly attachedToId?: string;
|
||||
/**
|
||||
* Optional offset in projection (wall-elevation) space, in meters.
|
||||
* `projectionOffsetX` is along the wall (positive = right of anchor) and
|
||||
* `projectionOffsetY` is in projection-pixel direction (positive = down).
|
||||
* Stored separately from `x`/`y` so dragging in the elevation view does not
|
||||
* corrupt the floor-plan offset.
|
||||
*/
|
||||
readonly projectionOffsetX?: number;
|
||||
readonly projectionOffsetY?: number;
|
||||
}
|
||||
|
||||
// ── Batch sync DTOs ──
|
||||
@@ -196,3 +408,31 @@ export interface BatchSyncFurnitureDto {
|
||||
readonly update: readonly { readonly id: string; readonly data: UpdateFurnitureItemDto }[];
|
||||
readonly delete: readonly string[];
|
||||
}
|
||||
|
||||
export interface CreateAnnotationDto {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly text: string;
|
||||
readonly fontSize?: number;
|
||||
readonly color?: string;
|
||||
readonly attachedToId?: string | null;
|
||||
readonly projectionOffsetX?: number | null;
|
||||
readonly projectionOffsetY?: number | null;
|
||||
}
|
||||
|
||||
export interface UpdateAnnotationDto {
|
||||
readonly x?: number;
|
||||
readonly y?: number;
|
||||
readonly text?: string;
|
||||
readonly fontSize?: number;
|
||||
readonly color?: string;
|
||||
readonly attachedToId?: string | null;
|
||||
readonly projectionOffsetX?: number | null;
|
||||
readonly projectionOffsetY?: number | null;
|
||||
}
|
||||
|
||||
export interface BatchSyncAnnotationsDto {
|
||||
readonly create: readonly CreateAnnotationDto[];
|
||||
readonly update: readonly { readonly id: string; readonly data: UpdateAnnotationDto }[];
|
||||
readonly delete: readonly string[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Wall, WallOpening, ElectricalItem, FurnitureItem } from './elements.js';
|
||||
import type { Wall, WallOpening, ElectricalItem, FurnitureItem, Annotation } from './elements.js';
|
||||
|
||||
export interface Point {
|
||||
readonly x: number;
|
||||
@@ -17,6 +17,29 @@ export const FLOOR_TYPES = [
|
||||
] as const;
|
||||
export type FloorType = (typeof FLOOR_TYPES)[number];
|
||||
|
||||
/**
|
||||
* How walls are visually finished in the 3D view. PAINT uses the room's
|
||||
* `wallColor` flat-tinted; the textured finishes ignore wallColor and apply
|
||||
* a real PBR material instead.
|
||||
*/
|
||||
export const WALL_FINISHES = [
|
||||
'PAINT',
|
||||
'PLASTER',
|
||||
'BRICK',
|
||||
'CONCRETE',
|
||||
'WOOD_PANEL',
|
||||
'WALLPAPER',
|
||||
] as const;
|
||||
export type WallFinish = (typeof WALL_FINISHES)[number];
|
||||
|
||||
/**
|
||||
* Default physical dimensions of a single outlet face plate (meters).
|
||||
* Used when a room does not override these values. ~7 cm matches a typical
|
||||
* European single outlet faceplate.
|
||||
*/
|
||||
export const DEFAULT_OUTLET_WIDTH = 0.07;
|
||||
export const DEFAULT_OUTLET_HEIGHT = 0.07;
|
||||
|
||||
export interface Room {
|
||||
readonly id: string;
|
||||
readonly apartmentId: string;
|
||||
@@ -32,6 +55,11 @@ export interface Room {
|
||||
readonly posY: number;
|
||||
readonly floorType: FloorType;
|
||||
readonly wallColor: string;
|
||||
readonly wallFinish: WallFinish;
|
||||
/** Physical width of a single outlet face plate (meters), used to draw outlet boundaries. */
|
||||
readonly outletWidth: number;
|
||||
/** Physical height of a single outlet face plate (meters), used to draw outlet boundaries. */
|
||||
readonly outletHeight: number;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
@@ -41,6 +69,7 @@ export interface RoomFull extends Room {
|
||||
readonly openings: readonly WallOpening[];
|
||||
readonly electricalItems: readonly ElectricalItem[];
|
||||
readonly furnitureItems: readonly FurnitureItem[];
|
||||
readonly annotations: readonly Annotation[];
|
||||
}
|
||||
|
||||
export interface CreateRoomDto {
|
||||
@@ -56,6 +85,9 @@ export interface CreateRoomDto {
|
||||
readonly posY?: number;
|
||||
readonly floorType?: FloorType;
|
||||
readonly wallColor?: string;
|
||||
readonly wallFinish?: WallFinish;
|
||||
readonly outletWidth?: number;
|
||||
readonly outletHeight?: number;
|
||||
}
|
||||
|
||||
export interface UpdateRoomDto {
|
||||
@@ -71,4 +103,7 @@ export interface UpdateRoomDto {
|
||||
readonly posY?: number;
|
||||
readonly floorType?: FloorType;
|
||||
readonly wallColor?: string;
|
||||
readonly wallFinish?: WallFinish;
|
||||
readonly outletWidth?: number;
|
||||
readonly outletHeight?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user