diff --git a/dist/index.html b/dist/index.html index e3022c7..d03278a 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,7 +5,7 @@ furniture-app - + diff --git a/src/ai/nn.ts b/src/ai/nn.ts index 98b6ef7..d5fcb2e 100644 --- a/src/ai/nn.ts +++ b/src/ai/nn.ts @@ -1,6 +1,7 @@ import type { LayoutEvaluation, LayoutState } from '../types'; import { evaluateLayout } from '../layout/evaluator'; -import { entranceRect, findFreeSpot, overlapsAny, resolveMove } from '../geometry/bounds'; +import { entranceCollider, findFreeSpot, furnitureCollider, overlapsAny, resolveMove } from '../geometry/bounds'; +import type { Collider } from '../geometry/collision'; export interface NNOptions { gridSize?: number; @@ -57,14 +58,21 @@ export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { b if (targetX < 0) targetX = 0; if (targetY < 0) targetY = 0; const room = candidate.room; - const others: any[] = candidate.furniture + const others: Collider[] = candidate.furniture .filter((o) => o.id !== f.id) - .map((o) => ({ x: o.shape.x, y: o.shape.y, width: o.shape.width, height: o.shape.height })); - const ent = entranceRect(candidate); + .map(furnitureCollider); + const ent = entranceCollider(candidate); if (ent) others.push(ent); - const b = overlapsAny({ x: targetX, y: targetY, width: f.shape.width, height: f.shape.height }, others) + const moving: Collider = { + x: targetX, + y: targetY, + width: f.shape.width, + height: f.shape.height, + rotation: f.shape.rotation ?? 0, + }; + const b = overlapsAny(moving, others) ? findFreeSpot(room, { width: f.shape.width, height: f.shape.height }, others) - : resolveMove({ x: targetX, y: targetY, width: f.shape.width, height: f.shape.height }, others, room); + : resolveMove(moving, others, room); f.shape.x = b.x; f.shape.y = b.y; f.shape.width = b.width; diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 3a51a3d..c031499 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -189,6 +189,26 @@ export function ControlPanel({ onChange={(v) => updateObject(p.id, { shape: { ...p.shape, height: v } })} /> + +
diff --git a/src/components/RoomCanvas.tsx b/src/components/RoomCanvas.tsx index 4d43499..75986ad 100644 --- a/src/components/RoomCanvas.tsx +++ b/src/components/RoomCanvas.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import type { LayoutState, Point } from '../types'; import { computePaths } from '../pathfinding/paths'; -import { resolveMove } from '../geometry/bounds'; +import { furnitureCollider, resolveMove } from '../geometry/bounds'; import { makePlaced } from '../optimization/prepare'; +import { Shape } from '../geometry/shapes'; interface RoomCanvasProps { state: LayoutState; @@ -115,14 +116,30 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s } for (const f of state.furniture) { - const b = { x: f.shape.x, y: f.shape.y, w: f.shape.width, h: f.shape.height }; - const r = Math.min(8, b.w / 4, b.h / 4); const selected = selectedId === f.id; + const cx = f.shape.x + f.shape.width / 2; + const cy = f.shape.y + f.shape.height / 2; + const rot = ((f.shape.rotation ?? 0) * Math.PI) / 180; + const isOval = f.shape.type === 'oval' || f.shape.type === 'circle'; ctx.globalAlpha = f.lockedForOptimization && mode === 'optimize' ? 0.55 : 1; + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(rot); ctx.beginPath(); - roundRect(ctx, b.x, b.y, b.w, b.h, r); + if (isOval) { + ctx.ellipse(0, 0, f.shape.width / 2, f.shape.height / 2, 0, 0, Math.PI * 2); + } else { + roundRect( + ctx, + -f.shape.width / 2, + -f.shape.height / 2, + f.shape.width, + f.shape.height, + Math.min(6, f.shape.width / 2, f.shape.height / 2), + ); + } ctx.fillStyle = f.color || '#9aa0a6'; ctx.fill(); @@ -138,17 +155,18 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s ctx.lineWidth = 1; ctx.stroke(); } + ctx.restore(); ctx.fillStyle = 'rgba(0, 0, 0, 0.72)'; ctx.font = '600 11px Inter, sans-serif'; ctx.textBaseline = 'middle'; - const label = b.w > 44 ? f.name.substring(0, 14) : ''; - if (label) ctx.fillText(label, b.x + 7, b.y + b.h / 2); + const label = f.shape.width > 44 ? f.name.substring(0, 14) : ''; + if (label) ctx.fillText(label, f.shape.x + 7, cy); if (f.lockedForOptimization) { ctx.font = '10px Inter, sans-serif'; ctx.fillStyle = 'rgba(0, 0, 0, 0.55)'; - ctx.fillText('🔒', b.x + b.w - 15, b.y + 9); + ctx.fillText('🔒', f.shape.x + f.shape.width - 15, f.shape.y + 9); } ctx.globalAlpha = 1; @@ -187,7 +205,8 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s } for (let i = state.furniture.length - 1; i >= 0; i--) { const f = state.furniture[i]; - if (pointInRect(pos, { x: f.shape.x, y: f.shape.y, w: f.shape.width, h: f.shape.height })) { + const shape = new Shape(f.shape); + if (shape.containsPoint(pos)) { onSelect(f.id); setDrag({ id: f.id, offsetX: pos.x - f.shape.x, offsetY: pos.y - f.shape.y }); return; @@ -202,12 +221,7 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s const pos = getMousePos(e); if (drag.id === '__entrance__' && state.entrance) { const en = state.entrance; - const others = state.furniture.map((o) => ({ - x: o.shape.x, - y: o.shape.y, - width: o.shape.width, - height: o.shape.height, - })); + const others = state.furniture.map(furnitureCollider); if (drag.resizing) { const b = resolveMove( { x: en.x, y: en.y, width: pos.x - en.x, height: pos.y - en.y }, @@ -229,9 +243,7 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s } const updated = state.furniture.map((f) => { if (f.id !== drag.id) return f; - const others: any[] = state.furniture - .filter((o) => o.id !== drag.id) - .map((o) => ({ x: o.shape.x, y: o.shape.y, width: o.shape.width, height: o.shape.height })); + const others = state.furniture.filter((o) => o.id !== drag.id).map(furnitureCollider); if (state.entrance) { const e = state.entrance; others.push({ x: e.x, y: e.y, width: e.width ?? 20, height: e.height ?? 20 }); @@ -242,6 +254,7 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s y: pos.y - drag.offsetY, width: f.shape.width, height: f.shape.height, + rotation: f.shape.rotation ?? 0, }, others, state.room, @@ -278,7 +291,18 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s const preset = (state.presets || []).find((p) => p.id === presetId); if (!preset) return; const pos = getMousePos(e); - const f = makePlaced(preset, pos.x - preset.shape.width / 2, pos.y - preset.shape.height / 2, state.room); + const others: import('../geometry/collision').Collider[] = state.furniture.map(furnitureCollider); + if (state.entrance) { + const en = state.entrance; + others.push({ x: en.x, y: en.y, width: en.width ?? 20, height: en.height ?? 20 }); + } + const f = makePlaced( + preset, + pos.x - preset.shape.width / 2, + pos.y - preset.shape.height / 2, + state.room, + others, + ); onStateChange({ ...state, furniture: [...state.furniture, f] }); onSelect(f.id); }; diff --git a/src/geometry/bounds.ts b/src/geometry/bounds.ts index 11e4929..4e64ad1 100644 --- a/src/geometry/bounds.ts +++ b/src/geometry/bounds.ts @@ -1,4 +1,8 @@ -import type { Entrance, FurnitureConfig, LayoutState, Rect, RoomConfig, Size } from '../types'; +import type { LayoutState, RoomConfig } from '../types'; +import { colliderCenter, satMTV, type Collider } from './collision'; +import { overlapsAny } from './collision'; + +export { overlapsAny }; export interface Bounds { x: number; @@ -7,24 +11,24 @@ export interface Bounds { height: number; } -export function clampRectToRoom(rect: Bounds, room: RoomConfig, minSize = 10): Bounds { +export function clampRectToRoom(rect: Collider, room: RoomConfig, minSize = 10): Collider { const width = Math.min(Math.max(minSize, rect.width), room.width); const height = Math.min(Math.max(minSize, rect.height), room.height); const x = Math.min(Math.max(0, rect.x), Math.max(0, room.width - width)); const y = Math.min(Math.max(0, rect.y), Math.max(0, room.height - height)); - return { x, y, width, height }; + return { ...rect, x, y, width, height }; } export function clampStateToRoom(state: LayoutState): LayoutState { const room = state.room; - const furniture: FurnitureConfig[] = state.furniture.map((f) => { + const furniture = state.furniture.map((f) => { const b = clampRectToRoom( { x: f.shape.x, y: f.shape.y, width: f.shape.width, height: f.shape.height }, room, ); - return { ...f, shape: { ...f.shape, ...b } }; + return { ...f, shape: { ...f.shape, x: b.x, y: b.y, width: b.width, height: b.height } }; }); - let entrance: Entrance | null = state.entrance; + let entrance = state.entrance; if (entrance) { const ev = clampRectToRoom( { x: entrance.x, y: entrance.y, width: entrance.width ?? 20, height: entrance.height ?? 20 }, @@ -36,32 +40,22 @@ export function clampStateToRoom(state: LayoutState): LayoutState { return { ...state, furniture, entrance }; } -export function rectsOverlap(a: Rect, b: Rect): boolean { - return !(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y); -} - -export function overlapsAny(rect: Rect, others: Rect[]): boolean { - return others.some((o) => rectsOverlap(rect, o)); -} - -export function resolveMove(moving: Bounds, others: Rect[], room: RoomConfig, minSize = 10): Bounds { +export function resolveMove(moving: Collider, others: Collider[], room: RoomConfig, minSize = 10): Collider { let r = clampRectToRoom(moving, room, minSize); for (let iter = 0; iter < 12; iter++) { let pushed = false; for (const o of others) { - if (!rectsOverlap(r, o)) continue; - const overlapLeft = r.x + r.width - o.x; - const overlapRight = o.x + o.width - r.x; - const overlapTop = r.y + r.height - o.y; - const overlapBottom = o.y + o.height - r.y; - const minX = Math.min(overlapLeft, overlapRight); - const minY = Math.min(overlapTop, overlapBottom); - if (minX < minY) { - r.x = overlapLeft < overlapRight ? o.x - r.width : o.x + o.width; - } else { - r.y = overlapTop < overlapBottom ? o.y - r.height : o.y + o.height; - } - r = clampRectToRoom(r, room, minSize); + const mtv = satMTV(r, o); + if (!mtv) continue; + const cM = colliderCenter(r); + const cO = colliderCenter(o); + const dir = (cM.x - cO.x) * mtv.axis.x + (cM.y - cO.y) * mtv.axis.y; + const sign = dir >= 0 ? 1 : -1; + r = clampRectToRoom( + { ...r, x: r.x + sign * mtv.axis.x * mtv.depth, y: r.y + sign * mtv.axis.y * mtv.depth }, + room, + minSize, + ); pushed = true; } if (!pushed) break; @@ -69,7 +63,7 @@ export function resolveMove(moving: Bounds, others: Rect[], room: RoomConfig, mi return r; } -export function findFreeSpot(room: RoomConfig, size: Size, others: Rect[], tries = 40): Bounds { +export function findFreeSpot(room: RoomConfig, size: { width: number; height: number }, others: Collider[], tries = 40): Collider { for (let i = 0; i < tries; i++) { const candidate = clampRectToRoom( { @@ -84,13 +78,10 @@ export function findFreeSpot(room: RoomConfig, size: Size, others: Rect[], tries const resolved = resolveMove(candidate, others, room); if (!overlapsAny(resolved, others)) return resolved; } - return clampRectToRoom( - { x: 0, y: 0, width: size.width, height: size.height }, - room, - ); + return clampRectToRoom({ x: 0, y: 0, width: size.width, height: size.height }, room); } -export function entranceRect(state: LayoutState): Rect | null { +export function entranceCollider(state: LayoutState): Collider | null { if (!state.entrance) return null; return { x: state.entrance.x, @@ -99,3 +90,7 @@ export function entranceRect(state: LayoutState): Rect | null { height: state.entrance.height ?? 20, }; } + +export function furnitureCollider(f: { shape: { x: number; y: number; width: number; height: number; rotation?: number } }): Collider { + return { x: f.shape.x, y: f.shape.y, width: f.shape.width, height: f.shape.height, rotation: f.shape.rotation }; +} diff --git a/src/geometry/collision.ts b/src/geometry/collision.ts new file mode 100644 index 0000000..ca96eed --- /dev/null +++ b/src/geometry/collision.ts @@ -0,0 +1,94 @@ +import type { Point } from '../types'; + +export interface Collider { + x: number; + y: number; + width: number; + height: number; + rotation?: number; +} + +export function colliderCenter(c: Collider): Point { + return { x: c.x + c.width / 2, y: c.y + c.height / 2 }; +} + +export function colliderCorners(c: Collider): Point[] { + const cx = c.x + c.width / 2; + const cy = c.y + c.height / 2; + const rad = ((c.rotation ?? 0) * Math.PI) / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const hw = c.width / 2; + const hh = c.height / 2; + const local: Point[] = [ + { x: -hw, y: -hh }, + { x: hw, y: -hh }, + { x: hw, y: hh }, + { x: -hw, y: hh }, + ]; + return local.map((p) => ({ + x: cx + p.x * cos - p.y * sin, + y: cy + p.x * sin + p.y * cos, + })); +} + +function project(points: Point[], axis: Point): [number, number] { + let min = Infinity; + let max = -Infinity; + for (const p of points) { + const d = p.x * axis.x + p.y * axis.y; + if (d < min) min = d; + if (d > max) max = d; + } + return [min, max]; +} + +function axesOf(corners: Point[]): Point[] { + const edges: Point[] = [ + { x: corners[1].x - corners[0].x, y: corners[1].y - corners[0].y }, + { x: corners[3].x - corners[0].x, y: corners[3].y - corners[0].y }, + ]; + return edges.map((e) => { + const len = Math.hypot(e.x, e.y) || 1; + return { x: -e.y / len, y: e.x / len }; + }); +} + +export function satMTV(a: Collider, b: Collider): { axis: Point; depth: number } | null { + const ca = colliderCorners(a); + const cb = colliderCorners(b); + const axes = [...axesOf(ca), ...axesOf(cb)]; + let minOverlap = Infinity; + let axis: Point = { x: 0, y: 0 }; + for (const ax of axes) { + const [amin, amax] = project(ca, ax); + const [bmin, bmax] = project(cb, ax); + const overlap = Math.min(amax, bmax) - Math.max(amin, bmin); + if (overlap <= 0) return null; + if (overlap < minOverlap) { + minOverlap = overlap; + axis = ax; + } + } + return { axis, depth: minOverlap }; +} + +export function collidersOverlap(a: Collider, b: Collider): boolean { + return satMTV(a, b) !== null; +} + +export function overlapsAny(c: Collider, others: Collider[]): boolean { + return others.some((o) => collidersOverlap(c, o)); +} + +export function containsWorldPoint(c: Collider, p: Point): boolean { + const center = colliderCenter(c); + const rad = (-(c.rotation ?? 0) * Math.PI) / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const dx = p.x - center.x; + const dy = p.y - center.y; + const lx = dx * cos - dy * sin; + const ly = dx * sin + dy * cos; + return Math.abs(lx) <= c.width / 2 && Math.abs(ly) <= c.height / 2; +} diff --git a/src/geometry/shapes.ts b/src/geometry/shapes.ts index debc564..40fa0bc 100644 --- a/src/geometry/shapes.ts +++ b/src/geometry/shapes.ts @@ -1,4 +1,5 @@ import type { Point, Rect, ShapeData } from '../types'; +import { colliderCorners, containsWorldPoint } from './collision'; export class Shape { type: ShapeData['type']; @@ -10,7 +11,7 @@ export class Shape { points: Point[]; constructor(data: ShapeData) { - this.type = data.type; + this.type = data.type === 'circle' ? 'oval' : data.type; this.x = data.x; this.y = data.y; this.width = Math.max(1, data.width); @@ -31,15 +32,12 @@ export class Shape { }); } + getCenter(): Point { + return { x: this.x + this.width / 2, y: this.y + this.height / 2 }; + } + getBounds(): Rect { - if (this.type === 'rectangle') { - return { x: this.x, y: this.y, width: this.width, height: this.height }; - } - if (this.type === 'circle') { - const r = Math.max(this.width, this.height) / 2; - return { x: this.x - r, y: this.y - r, width: r * 2, height: r * 2 }; - } - if (this.points.length > 0) { + if (this.type === 'polygon' && this.points.length > 0) { const xs = this.points.map((p) => p.x); const ys = this.points.map((p) => p.y); const minX = Math.min(...xs); @@ -48,22 +46,26 @@ export class Shape { const maxY = Math.max(...ys); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } - return { x: this.x, y: this.y, width: this.width, height: this.height }; + if (this.rotation % 360 === 0) { + return { x: this.x, y: this.y, width: this.width, height: this.height }; + } + const corners = colliderCorners({ + x: this.x, + y: this.y, + width: this.width, + height: this.height, + rotation: this.rotation, + }); + const xs = corners.map((p) => p.x); + const ys = corners.map((p) => p.y); + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } containsPoint(p: Point): boolean { - const b = this.getBounds(); - if (p.x < b.x || p.x > b.x + b.width || p.y < b.y || p.y > b.y + b.height) { - if (this.type !== 'polygon') return false; - } - if (this.type === 'circle') { - const cx = b.x + b.width / 2; - const cy = b.y + b.height / 2; - const r = b.width / 2; - const dx = p.x - cx; - const dy = p.y - cy; - return dx * dx + dy * dy <= r * r + 1e-8; - } if (this.type === 'polygon' && this.points.length > 0) { let inside = false; for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) { @@ -76,11 +78,20 @@ export class Shape { } return inside; } - return p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height; - } - - intersectsRect(r: Rect): boolean { - const b = this.getBounds(); - return !(r.x + r.width < b.x || r.x > b.x + b.width || r.y + r.height < b.y || r.y > b.y + b.height); + const c = { x: this.x, y: this.y, width: this.width, height: this.height, rotation: this.rotation }; + if (this.type === 'oval') { + const center = this.getCenter(); + const rad = (-this.rotation * Math.PI) / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const dx = p.x - center.x; + const dy = p.y - center.y; + const lx = dx * cos - dy * sin; + const ly = dx * sin + dy * cos; + const rx = this.width / 2; + const ry = this.height / 2; + return (lx * lx) / (rx * rx) + (ly * ly) / (ry * ry) <= 1 + 1e-8; + } + return containsWorldPoint(c, p); } } diff --git a/src/layout/evaluator.ts b/src/layout/evaluator.ts index f2b93b6..aae5e6e 100644 --- a/src/layout/evaluator.ts +++ b/src/layout/evaluator.ts @@ -1,7 +1,8 @@ import type { AccessibilityResult, FurnitureConfig, LayoutEvaluation, LayoutState, Point, Rect } from '../types'; import { buildGrid, gridToWorld, nearestWalkable } from '../pathfinding/grid'; import { aStar } from '../pathfinding/aStar'; -import { entranceRect, rectsOverlap } from '../geometry/bounds'; +import { entranceCollider, furnitureCollider } from '../geometry/bounds'; +import { collidersOverlap } from '../geometry/collision'; export function evaluateLayout(state: LayoutState, gridSize: number = 10): LayoutEvaluation { const obstacles: Rect[] = state.furniture @@ -19,15 +20,16 @@ export function evaluateLayout(state: LayoutState, gridSize: number = 10): Layou let totalConditionScore = 0; const conditionScores: Record = {}; - for (let i = 0; i < obstacles.length; i++) { - for (let j = i + 1; j < obstacles.length; j++) { - if (rectsOverlap(obstacles[i], obstacles[j])) penalty += 20; + const colliders = state.furniture.map(furnitureCollider); + for (let i = 0; i < colliders.length; i++) { + for (let j = i + 1; j < colliders.length; j++) { + if (collidersOverlap(colliders[i], colliders[j])) penalty += 20; } } - const entrance = entranceRect(state); + const entrance = entranceCollider(state); if (entrance) { - for (const o of obstacles) { - if (rectsOverlap(o, entrance)) penalty += 20; + for (const c of colliders) { + if (collidersOverlap(c, entrance)) penalty += 20; } } diff --git a/src/optimization/prepare.ts b/src/optimization/prepare.ts index 865d779..95af04d 100644 --- a/src/optimization/prepare.ts +++ b/src/optimization/prepare.ts @@ -1,18 +1,19 @@ -import type { FurnitureConfig, FurniturePreset, LayoutState, Rect, RoomConfig } from '../types'; -import { clampStateToRoom, entranceRect, findFreeSpot, resolveMove } from '../geometry/bounds'; +import type { FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types'; +import { clampStateToRoom, entranceCollider, findFreeSpot, furnitureCollider, resolveMove } from '../geometry/bounds'; +import type { Collider } from '../geometry/collision'; export function makeGenerated( preset: FurniturePreset, index: number, room: RoomConfig, - others: Rect[] = [], + others: Collider[] = [], ): FurnitureConfig { const b = findFreeSpot(room, { width: preset.shape.width, height: preset.shape.height }, others); return { id: `${preset.id}_gen_${Date.now()}_${index}`, name: preset.name, type: preset.id, - shape: { ...preset.shape, ...b }, + shape: { ...preset.shape, x: b.x, y: b.y, width: b.width, height: b.height }, conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), color: preset.color, placed: false, @@ -24,14 +25,18 @@ export function makePlaced( x: number, y: number, room: RoomConfig, - others: Rect[] = [], + others: Collider[] = [], ): FurnitureConfig { - const b = resolveMove({ x, y, width: preset.shape.width, height: preset.shape.height }, others, room); + const b = resolveMove( + { x, y, width: preset.shape.width, height: preset.shape.height, rotation: preset.shape.rotation }, + others, + room, + ); return { id: `${preset.id}_placed_${Date.now()}_${Math.round(Math.random() * 1e6)}`, name: preset.name, type: preset.id, - shape: { ...preset.shape, ...b }, + shape: { ...preset.shape, x: b.x, y: b.y, width: b.width, height: b.height }, conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), color: preset.color, placed: true, @@ -41,7 +46,7 @@ export function makePlaced( export function prepareForOptimize(state: LayoutState): LayoutState { const presets = state.presets || []; - const entrance = entranceRect(state); + const entrance = entranceCollider(state); let furniture = [...state.furniture]; for (const preset of presets) { @@ -55,12 +60,12 @@ export function prepareForOptimize(state: LayoutState): LayoutState { generated = generated.slice(0, need); } while (generated.length < need) { - const others: Rect[] = [ + const others: Collider[] = [ ...placed, ...generated, ...furniture.filter((f) => f.type !== preset.id), - ...(entrance ? [entrance] : []), - ].map((f: any) => ({ x: f.shape.x, y: f.shape.y, width: f.shape.width, height: f.shape.height })); + ].map(furnitureCollider); + if (entrance) others.push(entrance); generated.push(makeGenerated(preset, generated.length, state.room, others)); } @@ -68,7 +73,13 @@ export function prepareForOptimize(state: LayoutState): LayoutState { ...f, name: preset.name, color: preset.color, - shape: { ...f.shape, width: preset.shape.width, height: preset.shape.height }, + shape: { + ...f.shape, + type: preset.shape.type, + width: preset.shape.width, + height: preset.shape.height, + rotation: preset.shape.rotation ?? 0, + }, conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), lockedForOptimization: !!f.placed, })); diff --git a/src/optimization/randomSearch.ts b/src/optimization/randomSearch.ts index f7f6580..03080ae 100644 --- a/src/optimization/randomSearch.ts +++ b/src/optimization/randomSearch.ts @@ -1,7 +1,8 @@ -import type { LayoutEvaluation, LayoutState, Rect } from '../types'; +import type { LayoutEvaluation, LayoutState } from '../types'; import { evaluateLayout } from '../layout/evaluator'; import { Furniture } from '../furniture/Furniture'; -import { entranceRect, findFreeSpot } from '../geometry/bounds'; +import { entranceCollider, findFreeSpot, furnitureCollider } from '../geometry/bounds'; +import type { Collider } from '../geometry/collision'; export interface RandomSearchOptions { iterations: number; @@ -20,12 +21,12 @@ export function runRandomSearch( for (let i = 0; i < options.iterations; i++) { const candidate = cloneState(bestState); const room = { width: options.roomWidth, height: options.roomHeight }; - const entrance = entranceRect(candidate); + const entrance = entranceCollider(candidate); for (const f of candidate.furniture) { if (f.lockedForOptimization) continue; - const others: Rect[] = candidate.furniture + const others: Collider[] = candidate.furniture .filter((o) => o.id !== f.id) - .map((o) => ({ x: o.shape.x, y: o.shape.y, width: o.shape.width, height: o.shape.height })); + .map(furnitureCollider); if (entrance) others.push(entrance); const b = findFreeSpot(room, { width: f.shape.width, height: f.shape.height }, others); f.shape.x = b.x; diff --git a/src/types/index.ts b/src/types/index.ts index 7037eae..3cdd520 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -export type ShapeType = 'rectangle' | 'circle' | 'polygon'; +export type ShapeType = 'rectangle' | 'oval' | 'circle' | 'polygon'; export interface Point { x: number;