264 lines
7.2 KiB
TypeScript

import type { Point2D } from '@pascal-app/core'
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
export function clampPlanValue(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max)
}
export function rotatePlanVector(x: number, y: number, rotation: number): [number, number] {
const cos = Math.cos(rotation)
const sin = Math.sin(rotation)
return [x * cos + y * sin, -x * sin + y * cos]
}
export function getRotatedRectanglePolygon(
center: Point2D,
width: number,
depth: number,
rotation: number,
): Point2D[] {
const halfWidth = width / 2
const halfDepth = depth / 2
const corners: Array<[number, number]> = [
[-halfWidth, -halfDepth],
[halfWidth, -halfDepth],
[halfWidth, halfDepth],
[-halfWidth, halfDepth],
]
return corners.map(([localX, localY]) => {
const [offsetX, offsetY] = rotatePlanVector(localX, localY, rotation)
return {
x: center.x + offsetX,
y: center.y + offsetY,
}
})
}
export function interpolatePlanPoint(start: Point2D, end: Point2D, t: number): Point2D {
return {
x: start.x + (end.x - start.x) * t,
y: start.y + (end.y - start.y) * t,
}
}
export function getPlanPointDistance(start: Point2D, end: Point2D): number {
return Math.hypot(end.x - start.x, end.y - start.y)
}
export function movePlanPointTowards(start: Point2D, end: Point2D, distance: number): Point2D {
const totalDistance = getPlanPointDistance(start, end)
if (totalDistance <= Number.EPSILON || distance <= 0) {
return start
}
return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
}
export function getThickPlanLinePolygon(line: FloorplanLineSegment, thickness: number): Point2D[] {
const dx = line.end.x - line.start.x
const dy = line.end.y - line.start.y
const length = Math.hypot(dx, dy)
if (length <= Number.EPSILON || thickness <= 0) {
return [line.start, line.end, line.end, line.start]
}
const halfThickness = thickness / 2
const normalX = (-dy / length) * halfThickness
const normalY = (dx / length) * halfThickness
return [
{ x: line.start.x + normalX, y: line.start.y + normalY },
{ x: line.end.x + normalX, y: line.end.y + normalY },
{ x: line.end.x - normalX, y: line.end.y - normalY },
{ x: line.start.x - normalX, y: line.start.y - normalY },
]
}
export function getFloorplanSelectionBounds(
start: [number, number],
end: [number, number],
): FloorplanSelectionBounds {
return {
minX: Math.min(start[0], end[0]),
maxX: Math.max(start[0], end[0]),
minY: Math.min(start[1], end[1]),
maxY: Math.max(start[1], end[1]),
}
}
export function isPointInsideSelectionBounds(point: Point2D, bounds: FloorplanSelectionBounds) {
return (
point.x >= bounds.minX &&
point.x <= bounds.maxX &&
point.y >= bounds.minY &&
point.y <= bounds.maxY
)
}
export function isPointInsidePolygon(point: Point2D, polygon: Point2D[]) {
let isInside = false
for (
let currentIndex = 0, previousIndex = polygon.length - 1;
currentIndex < polygon.length;
previousIndex = currentIndex, currentIndex += 1
) {
const current = polygon[currentIndex]
const previous = polygon[previousIndex]
if (!(current && previous)) {
continue
}
const intersects =
current.y > point.y !== previous.y > point.y &&
point.x <
((previous.x - current.x) * (point.y - current.y)) / (previous.y - current.y) + current.x
if (intersects) {
isInside = !isInside
}
}
return isInside
}
export function isPointInsidePolygonWithHoles(
point: Point2D,
polygon: Point2D[],
holes: Point2D[][] = [],
) {
return (
isPointInsidePolygon(point, polygon) && !holes.some((hole) => isPointInsidePolygon(point, hole))
)
}
function getLineOrientation(start: Point2D, end: Point2D, point: Point2D) {
return (end.x - start.x) * (point.y - start.y) - (end.y - start.y) * (point.x - start.x)
}
function isPointOnSegment(point: Point2D, start: Point2D, end: Point2D) {
const epsilon = 1e-9
return (
Math.abs(getLineOrientation(start, end, point)) <= epsilon &&
point.x >= Math.min(start.x, end.x) - epsilon &&
point.x <= Math.max(start.x, end.x) + epsilon &&
point.y >= Math.min(start.y, end.y) - epsilon &&
point.y <= Math.max(start.y, end.y) + epsilon
)
}
function doSegmentsIntersect(
firstStart: Point2D,
firstEnd: Point2D,
secondStart: Point2D,
secondEnd: Point2D,
) {
const orientation1 = getLineOrientation(firstStart, firstEnd, secondStart)
const orientation2 = getLineOrientation(firstStart, firstEnd, secondEnd)
const orientation3 = getLineOrientation(secondStart, secondEnd, firstStart)
const orientation4 = getLineOrientation(secondStart, secondEnd, firstEnd)
const hasProperIntersection =
((orientation1 > 0 && orientation2 < 0) || (orientation1 < 0 && orientation2 > 0)) &&
((orientation3 > 0 && orientation4 < 0) || (orientation3 < 0 && orientation4 > 0))
if (hasProperIntersection) {
return true
}
return (
isPointOnSegment(secondStart, firstStart, firstEnd) ||
isPointOnSegment(secondEnd, firstStart, firstEnd) ||
isPointOnSegment(firstStart, secondStart, secondEnd) ||
isPointOnSegment(firstEnd, secondStart, secondEnd)
)
}
export function doesPolygonIntersectSelectionBounds(
polygon: Point2D[],
bounds: FloorplanSelectionBounds,
) {
if (polygon.length === 0) {
return false
}
if (polygon.some((point) => isPointInsideSelectionBounds(point, bounds))) {
return true
}
const boundsCorners: [Point2D, Point2D, Point2D, Point2D] = [
{ x: bounds.minX, y: bounds.minY },
{ x: bounds.maxX, y: bounds.minY },
{ x: bounds.maxX, y: bounds.maxY },
{ x: bounds.minX, y: bounds.maxY },
]
if (boundsCorners.some((corner) => isPointInsidePolygon(corner, polygon))) {
return true
}
const boundsEdges = [
[boundsCorners[0], boundsCorners[1]],
[boundsCorners[1], boundsCorners[2]],
[boundsCorners[2], boundsCorners[3]],
[boundsCorners[3], boundsCorners[0]],
] as const
for (let index = 0; index < polygon.length; index += 1) {
const start = polygon[index]
const end = polygon[(index + 1) % polygon.length]
if (!(start && end)) {
continue
}
for (const [edgeStart, edgeEnd] of boundsEdges) {
if (doSegmentsIntersect(start, end, edgeStart, edgeEnd)) {
return true
}
}
}
return false
}
export function getDistanceToWallSegment(
point: Point2D,
start: [number, number],
end: [number, number],
) {
const dx = end[0] - start[0]
const dy = end[1] - start[1]
const lengthSquared = dx * dx + dy * dy
if (lengthSquared <= Number.EPSILON) {
return Math.hypot(point.x - start[0], point.y - start[1])
}
const projection = clampPlanValue(
((point.x - start[0]) * dx + (point.y - start[1]) * dy) / lengthSquared,
0,
1,
)
const projectedX = start[0] + dx * projection
const projectedY = start[1] + dy * projection
return Math.hypot(point.x - projectedX, point.y - projectedY)
}
export function pointMatchesWallPlanPoint(
point: Point2D | undefined,
planPoint: [number, number],
epsilon = 1e-6,
): boolean {
if (!point) {
return false
}
return Math.abs(point.x - planPoint[0]) <= epsilon && Math.abs(point.y - planPoint[1]) <= epsilon
}