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:
2026-04-05 22:34:03 +03:00
parent b84807bbdb
commit af8b9fe00f
188 changed files with 35795 additions and 0 deletions
@@ -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();
});
});