feat: complete house plan maker application
Full-featured house/apartment floor plan editor with: - Turborepo monorepo (React/Vite client, Fastify/Prisma server, shared Zod schemas) - 2D room editor with walls, doors, windows, furniture, electrical elements - 3D room preview with Three.js (auto-hide nearest walls, bird's eye default) - Wall projection views with interactive drag (elevation, position) - Apartment floor plan view with room positioning - Copy/paste, alignment tools, measurement tool, annotations - Item-attached annotations with leader lines (visible on projections) - Door open direction (LEFT/RIGHT/INWARD/OUTWARD) with swing arc - Floor type textures (wood, tile, concrete, laminate, herringbone) - Wall color picker for 3D view - Furniture: bed, desk, wardrobe, sofa, table, chair, shelf, nightstand, dresser, bookcase, TV (with stand toggle), AC unit - Furniture elevation support (wall-mounted items) - Auto-save with dirty state tracking, batch save API - Rotation-aware collision detection (SAT/OBB) with 3D elevation check - Rotation-aware hit testing - i18n (English/Russian) with locale-aware number formatting - Dark mode with system preference detection - Undo/redo, keyboard shortcuts, scale bar - PDF/PNG/JSON export and JSON import - Focus trap modal, toast notifications, tooltips - Responsive layout with overlay palettes
This commit is contained in:
Generated
+2343
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@house-plan-maker/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"dev": "tsc --build --watch",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createApartmentSchema, updateApartmentSchema } from '../schemas/apartment.schema.js';
|
||||
import { createRoomSchema, updateRoomSchema } from '../schemas/room.schema.js';
|
||||
|
||||
describe('createApartmentSchema', () => {
|
||||
it('validates a valid apartment', () => {
|
||||
const result = createApartmentSchema.parse({
|
||||
name: 'Test Apartment',
|
||||
address: '123 Main St',
|
||||
totalArea: 75.5,
|
||||
});
|
||||
expect(result.name).toBe('Test Apartment');
|
||||
expect(result.address).toBe('123 Main St');
|
||||
expect(result.totalArea).toBe(75.5);
|
||||
});
|
||||
|
||||
it('requires name', () => {
|
||||
expect(() => createApartmentSchema.parse({})).toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty name', () => {
|
||||
expect(() => createApartmentSchema.parse({ name: '' })).toThrow();
|
||||
});
|
||||
|
||||
it('allows optional address and totalArea', () => {
|
||||
const result = createApartmentSchema.parse({ name: 'Test' });
|
||||
expect(result.name).toBe('Test');
|
||||
});
|
||||
|
||||
it('rejects negative totalArea', () => {
|
||||
expect(() => createApartmentSchema.parse({ name: 'Test', totalArea: -10 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateApartmentSchema', () => {
|
||||
it('allows partial update', () => {
|
||||
const result = updateApartmentSchema.parse({ name: 'Updated' });
|
||||
expect(result.name).toBe('Updated');
|
||||
});
|
||||
|
||||
it('allows empty object', () => {
|
||||
const result = updateApartmentSchema.parse({});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRoomSchema', () => {
|
||||
it('validates a valid room', () => {
|
||||
const result = createRoomSchema.parse({
|
||||
name: 'Bedroom',
|
||||
shape: [{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 4, y: 3 }, { x: 0, y: 3 }],
|
||||
width: 4,
|
||||
height: 3,
|
||||
wallHeight: 2.7,
|
||||
});
|
||||
expect(result.name).toBe('Bedroom');
|
||||
expect(result.shape).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('requires name', () => {
|
||||
expect(() => createRoomSchema.parse({})).toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty name', () => {
|
||||
expect(() => createRoomSchema.parse({ name: '' })).toThrow();
|
||||
});
|
||||
|
||||
it('allows optional fields', () => {
|
||||
const result = createRoomSchema.parse({ name: 'Room' });
|
||||
expect(result.name).toBe('Room');
|
||||
});
|
||||
|
||||
it('rejects negative width', () => {
|
||||
expect(() => createRoomSchema.parse({ name: 'Room', width: -1 })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects negative wallHeight', () => {
|
||||
expect(() => createRoomSchema.parse({ name: 'Room', wallHeight: -1 })).toThrow();
|
||||
});
|
||||
|
||||
it('allows zero plinthHeight', () => {
|
||||
const result = createRoomSchema.parse({ name: 'Room', plinthHeight: 0 });
|
||||
expect(result.plinthHeight).toBe(0);
|
||||
});
|
||||
|
||||
it('validates order as non-negative integer', () => {
|
||||
const result = createRoomSchema.parse({ name: 'Room', order: 0 });
|
||||
expect(result.order).toBe(0);
|
||||
expect(() => createRoomSchema.parse({ name: 'Room', order: -1 })).toThrow();
|
||||
expect(() => createRoomSchema.parse({ name: 'Room', order: 1.5 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRoomSchema', () => {
|
||||
it('allows partial update', () => {
|
||||
const result = updateRoomSchema.parse({ name: 'Updated Room' });
|
||||
expect(result.name).toBe('Updated Room');
|
||||
});
|
||||
|
||||
it('allows empty object', () => {
|
||||
const result = updateRoomSchema.parse({});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Shared types and utilities for the House Plan Maker application.
|
||||
*/
|
||||
|
||||
export interface HealthCheckResponse {
|
||||
status: 'ok' | 'error';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Types
|
||||
export type {
|
||||
Apartment,
|
||||
ApartmentWithRooms,
|
||||
ApartmentRoom,
|
||||
CreateApartmentDto,
|
||||
UpdateApartmentDto,
|
||||
} from './types/apartment.js';
|
||||
|
||||
export type {
|
||||
Point,
|
||||
FloorType,
|
||||
Room,
|
||||
RoomFull,
|
||||
CreateRoomDto,
|
||||
UpdateRoomDto,
|
||||
} from './types/room.js';
|
||||
|
||||
export { FLOOR_TYPES } from './types/room.js';
|
||||
|
||||
export type {
|
||||
WallDirection,
|
||||
Wall,
|
||||
CreateWallDto,
|
||||
OpeningType,
|
||||
DoorOpenDirection,
|
||||
WallOpening,
|
||||
CreateWallOpeningDto,
|
||||
UpdateWallOpeningDto,
|
||||
ElectricalType,
|
||||
ElectricalItem,
|
||||
CreateElectricalItemDto,
|
||||
UpdateElectricalItemDto,
|
||||
FurnitureType,
|
||||
FurnitureItem,
|
||||
CreateFurnitureItemDto,
|
||||
UpdateFurnitureItemDto,
|
||||
Annotation,
|
||||
BatchSyncOpeningsDto,
|
||||
BatchSyncElectricalDto,
|
||||
BatchSyncFurnitureDto,
|
||||
} from './types/elements.js';
|
||||
|
||||
export {
|
||||
WALL_DIRECTIONS,
|
||||
OPENING_TYPES,
|
||||
DOOR_OPEN_DIRECTIONS,
|
||||
ELECTRICAL_TYPES,
|
||||
FURNITURE_TYPES,
|
||||
} from './types/elements.js';
|
||||
|
||||
export type {
|
||||
ApiResponse,
|
||||
ApiListResponse,
|
||||
ApiErrorResponse,
|
||||
} from './types/api.js';
|
||||
|
||||
// Zod schemas
|
||||
export {
|
||||
createApartmentSchema,
|
||||
updateApartmentSchema,
|
||||
type CreateApartmentInput,
|
||||
type UpdateApartmentInput,
|
||||
} from './schemas/apartment.schema.js';
|
||||
|
||||
export {
|
||||
createRoomSchema,
|
||||
updateRoomSchema,
|
||||
type CreateRoomInput,
|
||||
type UpdateRoomInput,
|
||||
} from './schemas/room.schema.js';
|
||||
|
||||
export {
|
||||
bulkUpdateWallsSchema,
|
||||
createWallOpeningSchema,
|
||||
updateWallOpeningSchema,
|
||||
createElectricalItemSchema,
|
||||
updateElectricalItemSchema,
|
||||
createFurnitureItemSchema,
|
||||
updateFurnitureItemSchema,
|
||||
batchSyncOpeningsSchema,
|
||||
batchSyncElectricalSchema,
|
||||
batchSyncFurnitureSchema,
|
||||
type BulkUpdateWallsInput,
|
||||
type CreateWallOpeningInput,
|
||||
type UpdateWallOpeningInput,
|
||||
type CreateElectricalItemInput,
|
||||
type UpdateElectricalItemInput,
|
||||
type CreateFurnitureItemInput,
|
||||
type UpdateFurnitureItemInput,
|
||||
type BatchSyncOpeningsInput,
|
||||
type BatchSyncElectricalInput,
|
||||
type BatchSyncFurnitureInput,
|
||||
} from './schemas/elements.schema.js';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createApartmentSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
address: z.string().max(500).nullish(),
|
||||
totalArea: z.number().positive('Total area must be positive').nullish(),
|
||||
});
|
||||
|
||||
export const updateApartmentSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255).optional(),
|
||||
address: z.string().max(500).nullish(),
|
||||
totalArea: z.number().positive('Total area must be positive').nullish(),
|
||||
});
|
||||
|
||||
export type CreateApartmentInput = z.infer<typeof createApartmentSchema>;
|
||||
export type UpdateApartmentInput = z.infer<typeof updateApartmentSchema>;
|
||||
@@ -0,0 +1,148 @@
|
||||
import { z } from 'zod';
|
||||
import { WALL_DIRECTIONS, OPENING_TYPES, DOOR_OPEN_DIRECTIONS, ELECTRICAL_TYPES, FURNITURE_TYPES } from '../types/elements.js';
|
||||
|
||||
// ── Wall schemas ──
|
||||
|
||||
const wallDirectionEnum = z.enum(WALL_DIRECTIONS);
|
||||
|
||||
const createWallItemSchema = z.object({
|
||||
startX: z.number(),
|
||||
startY: z.number(),
|
||||
endX: z.number(),
|
||||
endY: z.number(),
|
||||
thickness: z.number().positive('Thickness must be positive').optional(),
|
||||
direction: wallDirectionEnum.optional(),
|
||||
});
|
||||
|
||||
/** Bulk update: replace all walls in a room. */
|
||||
export const bulkUpdateWallsSchema = z.object({
|
||||
walls: z.array(createWallItemSchema),
|
||||
});
|
||||
|
||||
export type BulkUpdateWallsInput = z.infer<typeof bulkUpdateWallsSchema>;
|
||||
|
||||
// ── WallOpening schemas ──
|
||||
|
||||
const openingTypeEnum = z.enum(OPENING_TYPES);
|
||||
const doorOpenDirectionEnum = z.enum(DOOR_OPEN_DIRECTIONS);
|
||||
|
||||
export const createWallOpeningSchema = z.object({
|
||||
wallId: z.string().min(1, 'Wall ID is required'),
|
||||
type: openingTypeEnum,
|
||||
positionAlongWall: z.number().min(0, 'Position must be non-negative'),
|
||||
width: z.number().positive('Width must be positive'),
|
||||
height: z.number().positive('Height must be positive'),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
openDirection: doorOpenDirectionEnum.optional(),
|
||||
});
|
||||
|
||||
export const updateWallOpeningSchema = z.object({
|
||||
type: openingTypeEnum.optional(),
|
||||
positionAlongWall: z.number().min(0, 'Position must be non-negative').optional(),
|
||||
width: z.number().positive('Width must be positive').optional(),
|
||||
height: z.number().positive('Height must be positive').optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
openDirection: doorOpenDirectionEnum.optional(),
|
||||
});
|
||||
|
||||
export type CreateWallOpeningInput = z.infer<typeof createWallOpeningSchema>;
|
||||
export type UpdateWallOpeningInput = z.infer<typeof updateWallOpeningSchema>;
|
||||
|
||||
// ── ElectricalItem schemas ──
|
||||
|
||||
const electricalTypeEnum = z.enum(ELECTRICAL_TYPES);
|
||||
|
||||
export const createElectricalItemSchema = z.object({
|
||||
type: electricalTypeEnum,
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
wallId: z.string().nullish(),
|
||||
elevationFromFloor: z.number().min(0).nullish(),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export const updateElectricalItemSchema = z.object({
|
||||
type: electricalTypeEnum.optional(),
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
wallId: z.string().nullish(),
|
||||
elevationFromFloor: z.number().min(0).nullish(),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export type CreateElectricalItemInput = z.infer<typeof createElectricalItemSchema>;
|
||||
export type UpdateElectricalItemInput = z.infer<typeof updateElectricalItemSchema>;
|
||||
|
||||
// ── FurnitureItem schemas ──
|
||||
|
||||
const furnitureTypeEnum = z.enum(FURNITURE_TYPES);
|
||||
|
||||
export const createFurnitureItemSchema = z.object({
|
||||
type: furnitureTypeEnum,
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number().positive('Width must be positive'),
|
||||
depth: z.number().positive('Depth must be positive'),
|
||||
height: z.number().positive('Height must be positive'),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
});
|
||||
|
||||
export const updateFurnitureItemSchema = z.object({
|
||||
type: furnitureTypeEnum.optional(),
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
width: z.number().positive('Width must be positive').optional(),
|
||||
depth: z.number().positive('Depth must be positive').optional(),
|
||||
height: z.number().positive('Height must be positive').optional(),
|
||||
rotation: z.number().min(0).max(360).optional(),
|
||||
elevationFromFloor: z.number().min(0).optional(),
|
||||
label: z.string().max(255).nullish(),
|
||||
});
|
||||
|
||||
export type CreateFurnitureItemInput = z.infer<typeof createFurnitureItemSchema>;
|
||||
export type UpdateFurnitureItemInput = z.infer<typeof updateFurnitureItemSchema>;
|
||||
|
||||
// ── Batch sync schemas ──
|
||||
|
||||
export const batchSyncOpeningsSchema = z.object({
|
||||
create: z.array(createWallOpeningSchema),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
data: updateWallOpeningSchema,
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string().min(1)),
|
||||
});
|
||||
|
||||
export type BatchSyncOpeningsInput = z.infer<typeof batchSyncOpeningsSchema>;
|
||||
|
||||
export const batchSyncElectricalSchema = z.object({
|
||||
create: z.array(createElectricalItemSchema),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
data: updateElectricalItemSchema,
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string().min(1)),
|
||||
});
|
||||
|
||||
export type BatchSyncElectricalInput = z.infer<typeof batchSyncElectricalSchema>;
|
||||
|
||||
export const batchSyncFurnitureSchema = z.object({
|
||||
create: z.array(createFurnitureItemSchema),
|
||||
update: z.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
data: updateFurnitureItemSchema,
|
||||
}),
|
||||
),
|
||||
delete: z.array(z.string().min(1)),
|
||||
});
|
||||
|
||||
export type BatchSyncFurnitureInput = z.infer<typeof batchSyncFurnitureSchema>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { FLOOR_TYPES } from '../types/room.js';
|
||||
|
||||
const floorTypeEnum = z.enum(FLOOR_TYPES);
|
||||
|
||||
const pointSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
export const createRoomSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
shape: z.array(pointSchema).optional(),
|
||||
width: z.number().positive('Width must be positive').nullish(),
|
||||
height: z.number().positive('Height must be positive').nullish(),
|
||||
wallHeight: z.number().positive('Wall height must be positive').optional(),
|
||||
plinthHeight: z.number().min(0, 'Plinth height must be non-negative').optional(),
|
||||
plinthThickness: z.number().min(0, 'Plinth thickness must be non-negative').optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
posX: z.number().optional(),
|
||||
posY: z.number().optional(),
|
||||
floorType: floorTypeEnum.optional(),
|
||||
wallColor: z.string().max(20).optional(),
|
||||
});
|
||||
|
||||
export const updateRoomSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255).optional(),
|
||||
shape: z.array(pointSchema).optional(),
|
||||
width: z.number().positive('Width must be positive').nullish(),
|
||||
height: z.number().positive('Height must be positive').nullish(),
|
||||
wallHeight: z.number().positive('Wall height must be positive').optional(),
|
||||
plinthHeight: z.number().min(0, 'Plinth height must be non-negative').optional(),
|
||||
plinthThickness: z.number().min(0, 'Plinth thickness must be non-negative').optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
posX: z.number().optional(),
|
||||
posY: z.number().optional(),
|
||||
floorType: floorTypeEnum.optional(),
|
||||
wallColor: z.string().max(20).optional(),
|
||||
});
|
||||
|
||||
export type CreateRoomInput = z.infer<typeof createRoomSchema>;
|
||||
export type UpdateRoomInput = z.infer<typeof updateRoomSchema>;
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface Apartment {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly address: string | null;
|
||||
readonly totalArea: number | null;
|
||||
readonly roomCount?: number;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ApartmentWithRooms extends Apartment {
|
||||
readonly rooms: readonly ApartmentRoom[];
|
||||
}
|
||||
|
||||
/** Lightweight room info when nested inside an apartment response. */
|
||||
export interface ApartmentRoom {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly order: number;
|
||||
}
|
||||
|
||||
export interface CreateApartmentDto {
|
||||
readonly name: string;
|
||||
readonly address?: string | null;
|
||||
readonly totalArea?: number | null;
|
||||
}
|
||||
|
||||
export interface UpdateApartmentDto {
|
||||
readonly name?: string;
|
||||
readonly address?: string | null;
|
||||
readonly totalArea?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Standard success response wrapper. */
|
||||
export interface ApiResponse<T> {
|
||||
readonly data: T;
|
||||
}
|
||||
|
||||
/** Standard list response wrapper. */
|
||||
export interface ApiListResponse<T> {
|
||||
readonly data: readonly T[];
|
||||
}
|
||||
|
||||
/** Standard error response. */
|
||||
export interface ApiErrorResponse {
|
||||
readonly error: string;
|
||||
readonly statusCode: number;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// ── Wall ──
|
||||
|
||||
export const WALL_DIRECTIONS = ['NORTH', 'SOUTH', 'EAST', 'WEST', 'OTHER'] as const;
|
||||
export type WallDirection = (typeof WALL_DIRECTIONS)[number];
|
||||
|
||||
export interface Wall {
|
||||
readonly id: string;
|
||||
readonly roomId: string;
|
||||
readonly startX: number;
|
||||
readonly startY: number;
|
||||
readonly endX: number;
|
||||
readonly endY: number;
|
||||
readonly thickness: number;
|
||||
readonly direction: WallDirection;
|
||||
}
|
||||
|
||||
export interface CreateWallDto {
|
||||
readonly startX: number;
|
||||
readonly startY: number;
|
||||
readonly endX: number;
|
||||
readonly endY: number;
|
||||
readonly thickness?: number;
|
||||
readonly direction?: WallDirection;
|
||||
}
|
||||
|
||||
// ── WallOpening ──
|
||||
|
||||
export const OPENING_TYPES = ['DOOR', 'WINDOW'] as const;
|
||||
export type OpeningType = (typeof OPENING_TYPES)[number];
|
||||
|
||||
export const DOOR_OPEN_DIRECTIONS = ['LEFT', 'RIGHT', 'INWARD', 'OUTWARD'] as const;
|
||||
export type DoorOpenDirection = (typeof DOOR_OPEN_DIRECTIONS)[number];
|
||||
|
||||
export interface WallOpening {
|
||||
readonly id: string;
|
||||
readonly roomId: string;
|
||||
readonly wallId: string;
|
||||
readonly type: OpeningType;
|
||||
readonly positionAlongWall: number;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
readonly elevationFromFloor: number;
|
||||
readonly openDirection: DoorOpenDirection;
|
||||
}
|
||||
|
||||
export interface CreateWallOpeningDto {
|
||||
readonly wallId: string;
|
||||
readonly type: OpeningType;
|
||||
readonly positionAlongWall: number;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly openDirection?: DoorOpenDirection;
|
||||
}
|
||||
|
||||
export interface UpdateWallOpeningDto {
|
||||
readonly type?: OpeningType;
|
||||
readonly positionAlongWall?: number;
|
||||
readonly width?: number;
|
||||
readonly height?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly openDirection?: DoorOpenDirection;
|
||||
}
|
||||
|
||||
// ── ElectricalItem ──
|
||||
|
||||
export const ELECTRICAL_TYPES = [
|
||||
'OUTLET',
|
||||
'SWITCH',
|
||||
'JUNCTION_BOX',
|
||||
'LIGHT_CEILING',
|
||||
'LIGHT_WALL',
|
||||
'CABLE_ROUTE',
|
||||
] as const;
|
||||
export type ElectricalType = (typeof ELECTRICAL_TYPES)[number];
|
||||
|
||||
export interface ElectricalItem {
|
||||
readonly id: string;
|
||||
readonly roomId: string;
|
||||
readonly type: ElectricalType;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly wallId: string | null;
|
||||
readonly elevationFromFloor: number | null;
|
||||
readonly rotation: number;
|
||||
readonly metadata: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface CreateElectricalItemDto {
|
||||
readonly type: ElectricalType;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly wallId?: string | null;
|
||||
readonly elevationFromFloor?: number | null;
|
||||
readonly rotation?: number;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface UpdateElectricalItemDto {
|
||||
readonly type?: ElectricalType;
|
||||
readonly x?: number;
|
||||
readonly y?: number;
|
||||
readonly wallId?: string | null;
|
||||
readonly elevationFromFloor?: number | null;
|
||||
readonly rotation?: number;
|
||||
readonly metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// ── FurnitureItem ──
|
||||
|
||||
export const FURNITURE_TYPES = [
|
||||
'BED',
|
||||
'DESK',
|
||||
'WARDROBE',
|
||||
'SOFA',
|
||||
'TABLE',
|
||||
'CHAIR',
|
||||
'SHELF',
|
||||
'NIGHTSTAND',
|
||||
'DRESSER',
|
||||
'BOOKCASE',
|
||||
'TV',
|
||||
'AC_UNIT',
|
||||
'OTHER',
|
||||
] as const;
|
||||
export type FurnitureType = (typeof FURNITURE_TYPES)[number];
|
||||
|
||||
export interface FurnitureItem {
|
||||
readonly id: string;
|
||||
readonly roomId: string;
|
||||
readonly type: FurnitureType;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly width: number;
|
||||
readonly depth: number;
|
||||
readonly height: number;
|
||||
readonly rotation: number;
|
||||
readonly elevationFromFloor: number;
|
||||
readonly label: string | null;
|
||||
}
|
||||
|
||||
export interface CreateFurnitureItemDto {
|
||||
readonly type: FurnitureType;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly width: number;
|
||||
readonly depth: number;
|
||||
readonly height: number;
|
||||
readonly rotation?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly label?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateFurnitureItemDto {
|
||||
readonly type?: FurnitureType;
|
||||
readonly x?: number;
|
||||
readonly y?: number;
|
||||
readonly width?: number;
|
||||
readonly depth?: number;
|
||||
readonly height?: number;
|
||||
readonly rotation?: number;
|
||||
readonly elevationFromFloor?: number;
|
||||
readonly label?: string | null;
|
||||
}
|
||||
|
||||
// ── Annotation ──
|
||||
|
||||
export interface Annotation {
|
||||
readonly id: string;
|
||||
readonly roomId: string;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly text: string;
|
||||
readonly fontSize?: number;
|
||||
readonly color?: string;
|
||||
/** If set, this annotation follows the item with this ID. x,y become offsets from the item. */
|
||||
readonly attachedToId?: string;
|
||||
}
|
||||
|
||||
// ── Batch sync DTOs ──
|
||||
|
||||
export interface BatchSyncOpeningsDto {
|
||||
readonly create: readonly CreateWallOpeningDto[];
|
||||
readonly update: readonly { readonly id: string; readonly data: UpdateWallOpeningDto }[];
|
||||
readonly delete: readonly string[];
|
||||
}
|
||||
|
||||
export interface BatchSyncElectricalDto {
|
||||
readonly create: readonly CreateElectricalItemDto[];
|
||||
readonly update: readonly { readonly id: string; readonly data: UpdateElectricalItemDto }[];
|
||||
readonly delete: readonly string[];
|
||||
}
|
||||
|
||||
export interface BatchSyncFurnitureDto {
|
||||
readonly create: readonly CreateFurnitureItemDto[];
|
||||
readonly update: readonly { readonly id: string; readonly data: UpdateFurnitureItemDto }[];
|
||||
readonly delete: readonly string[];
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Wall, WallOpening, ElectricalItem, FurnitureItem } from './elements.js';
|
||||
|
||||
export interface Point {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
|
||||
export const FLOOR_TYPES = [
|
||||
'CONCRETE',
|
||||
'WOOD_LIGHT',
|
||||
'WOOD_MEDIUM',
|
||||
'WOOD_DARK',
|
||||
'WOOD_HERRINGBONE',
|
||||
'TILE_WHITE',
|
||||
'TILE_GRAY',
|
||||
'LAMINATE',
|
||||
] as const;
|
||||
export type FloorType = (typeof FLOOR_TYPES)[number];
|
||||
|
||||
export interface Room {
|
||||
readonly id: string;
|
||||
readonly apartmentId: string;
|
||||
readonly name: string;
|
||||
readonly shape: readonly Point[];
|
||||
readonly width: number | null;
|
||||
readonly height: number | null;
|
||||
readonly wallHeight: number;
|
||||
readonly plinthHeight: number;
|
||||
readonly plinthThickness: number;
|
||||
readonly order: number;
|
||||
readonly posX: number;
|
||||
readonly posY: number;
|
||||
readonly floorType: FloorType;
|
||||
readonly wallColor: string;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RoomFull extends Room {
|
||||
readonly walls: readonly Wall[];
|
||||
readonly openings: readonly WallOpening[];
|
||||
readonly electricalItems: readonly ElectricalItem[];
|
||||
readonly furnitureItems: readonly FurnitureItem[];
|
||||
}
|
||||
|
||||
export interface CreateRoomDto {
|
||||
readonly name: string;
|
||||
readonly shape?: readonly Point[];
|
||||
readonly width?: number | null;
|
||||
readonly height?: number | null;
|
||||
readonly wallHeight?: number;
|
||||
readonly plinthHeight?: number;
|
||||
readonly plinthThickness?: number;
|
||||
readonly order?: number;
|
||||
readonly posX?: number;
|
||||
readonly posY?: number;
|
||||
readonly floorType?: FloorType;
|
||||
readonly wallColor?: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoomDto {
|
||||
readonly name?: string;
|
||||
readonly shape?: readonly Point[];
|
||||
readonly width?: number | null;
|
||||
readonly height?: number | null;
|
||||
readonly wallHeight?: number;
|
||||
readonly plinthHeight?: number;
|
||||
readonly plinthThickness?: number;
|
||||
readonly order?: number;
|
||||
readonly posX?: number;
|
||||
readonly posY?: number;
|
||||
readonly floorType?: FloorType;
|
||||
readonly wallColor?: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user