Extract floorplan render logic into editor 2D modules
This commit is contained in:
@@ -96,44 +96,6 @@ export {
|
|||||||
pointToKey,
|
pointToKey,
|
||||||
type WallMiterData,
|
type WallMiterData,
|
||||||
} from './systems/wall/wall-mitering'
|
} from './systems/wall/wall-mitering'
|
||||||
export {
|
|
||||||
clampPlanValue,
|
|
||||||
doesPolygonIntersectSelectionBounds,
|
|
||||||
getDistanceToWallSegment,
|
|
||||||
getFloorplanSelectionBounds,
|
|
||||||
getPlanPointDistance,
|
|
||||||
getRotatedRectanglePolygon,
|
|
||||||
getThickPlanLinePolygon,
|
|
||||||
interpolatePlanPoint,
|
|
||||||
isPointInsidePolygon,
|
|
||||||
isPointInsidePolygonWithHoles,
|
|
||||||
isPointInsideSelectionBounds,
|
|
||||||
movePlanPointTowards,
|
|
||||||
pointMatchesWallPlanPoint,
|
|
||||||
rotatePlanVector,
|
|
||||||
} from './plan/geometry'
|
|
||||||
export {
|
|
||||||
buildFloorplanItemEntry,
|
|
||||||
collectLevelDescendants,
|
|
||||||
getItemFloorplanTransform,
|
|
||||||
} from './plan/items'
|
|
||||||
export {
|
|
||||||
buildFloorplanStairEntry,
|
|
||||||
computeFloorplanStairSegmentTransforms,
|
|
||||||
getFloorplanStairSegmentPolygon,
|
|
||||||
} from './plan/stairs'
|
|
||||||
export type {
|
|
||||||
FloorplanItemEntry,
|
|
||||||
FloorplanLineSegment,
|
|
||||||
FloorplanNodeTransform,
|
|
||||||
FloorplanSelectionBounds,
|
|
||||||
FloorplanStairArrowEntry,
|
|
||||||
FloorplanStairEntry,
|
|
||||||
FloorplanStairSegmentEntry,
|
|
||||||
LevelDescendantMap,
|
|
||||||
StairSegmentTransform,
|
|
||||||
} from './plan/types'
|
|
||||||
export { getFloorplanWall, getFloorplanWallThickness } from './plan/walls'
|
|
||||||
export { WallSystem } from './systems/wall/wall-system'
|
export { WallSystem } from './systems/wall/wall-system'
|
||||||
export { WindowSystem } from './systems/window/window-system'
|
export { WindowSystem } from './systems/window/window-system'
|
||||||
export type { SceneGraph } from './utils/clone-scene-graph'
|
export type { SceneGraph } from './utils/clone-scene-graph'
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
const FLOORPLAN_MEASUREMENT_LINE_WIDTH = 1.35
|
||||||
|
const FLOORPLAN_MEASUREMENT_LINE_OPACITY = 0.95
|
||||||
|
const FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE = 0.15
|
||||||
|
const FLOORPLAN_MEASUREMENT_LABEL_OPACITY = 0.98
|
||||||
|
const FLOORPLAN_MEASUREMENT_EXTENSION_DASH = '0.08 0.12'
|
||||||
|
const FLOORPLAN_MEASUREMENT_END_TICK = 0.18
|
||||||
|
|
||||||
|
export type LinearMeasurementOverlay = {
|
||||||
|
id: string
|
||||||
|
dimensionLineEnd: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
dimensionLineStart: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
extensionStart: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
extensionEnd: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
label: string
|
||||||
|
labelX: number
|
||||||
|
labelY: number
|
||||||
|
labelAngleDeg: number
|
||||||
|
extensionStroke?: string
|
||||||
|
isSelected?: boolean
|
||||||
|
labelFill?: string
|
||||||
|
stroke?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanMeasurementPalette = {
|
||||||
|
measurementStroke: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanMeasurementLineProps = {
|
||||||
|
palette: FloorplanMeasurementPalette
|
||||||
|
segment: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
isSelected?: boolean
|
||||||
|
dashed?: boolean
|
||||||
|
stroke?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function FloorplanMeasurementLine({
|
||||||
|
palette,
|
||||||
|
segment,
|
||||||
|
isSelected,
|
||||||
|
dashed = false,
|
||||||
|
stroke,
|
||||||
|
}: FloorplanMeasurementLineProps) {
|
||||||
|
const lineOpacity = isSelected
|
||||||
|
? FLOORPLAN_MEASUREMENT_LINE_OPACITY
|
||||||
|
: FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
stroke={stroke ?? palette.measurementStroke}
|
||||||
|
strokeDasharray={dashed ? FLOORPLAN_MEASUREMENT_EXTENSION_DASH : undefined}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeOpacity={lineOpacity}
|
||||||
|
strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
x1={segment.x1}
|
||||||
|
x2={segment.x2}
|
||||||
|
y1={segment.y1}
|
||||||
|
y2={segment.y2}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanMeasurementTickProps = {
|
||||||
|
palette: FloorplanMeasurementPalette
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
angleDeg: number
|
||||||
|
isSelected?: boolean
|
||||||
|
stroke?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function FloorplanMeasurementTick({
|
||||||
|
palette,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
angleDeg,
|
||||||
|
isSelected,
|
||||||
|
stroke,
|
||||||
|
}: FloorplanMeasurementTickProps) {
|
||||||
|
const radians = (angleDeg * Math.PI) / 180
|
||||||
|
const nx = -Math.sin(radians)
|
||||||
|
const ny = Math.cos(radians)
|
||||||
|
const half = FLOORPLAN_MEASUREMENT_END_TICK / 2
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
stroke={stroke ?? palette.measurementStroke}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeOpacity={
|
||||||
|
isSelected ? FLOORPLAN_MEASUREMENT_LINE_OPACITY : FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
|
||||||
|
}
|
||||||
|
strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
x1={x - nx * half}
|
||||||
|
x2={x + nx * half}
|
||||||
|
y1={y - ny * half}
|
||||||
|
y2={y + ny * half}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanMeasurementsLayerProps = {
|
||||||
|
className: string
|
||||||
|
measurements: LinearMeasurementOverlay[]
|
||||||
|
palette: FloorplanMeasurementPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
|
||||||
|
className,
|
||||||
|
measurements,
|
||||||
|
palette,
|
||||||
|
}: FloorplanMeasurementsLayerProps) {
|
||||||
|
if (measurements.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{measurements.map((measurement) => (
|
||||||
|
<g className={className} key={measurement.id} pointerEvents="none" style={{ userSelect: 'none' }}>
|
||||||
|
<FloorplanMeasurementLine
|
||||||
|
dashed
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
segment={measurement.extensionStart}
|
||||||
|
stroke={measurement.extensionStroke}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementLine
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
segment={measurement.dimensionLineStart}
|
||||||
|
stroke={measurement.stroke}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementLine
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
segment={measurement.dimensionLineEnd}
|
||||||
|
stroke={measurement.stroke}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementLine
|
||||||
|
dashed
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
segment={measurement.extensionEnd}
|
||||||
|
stroke={measurement.extensionStroke}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementTick
|
||||||
|
angleDeg={measurement.labelAngleDeg}
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
stroke={measurement.stroke}
|
||||||
|
x={measurement.dimensionLineStart.x1}
|
||||||
|
y={measurement.dimensionLineStart.y1}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementTick
|
||||||
|
angleDeg={measurement.labelAngleDeg}
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
stroke={measurement.stroke}
|
||||||
|
x={measurement.dimensionLineEnd.x2}
|
||||||
|
y={measurement.dimensionLineEnd.y2}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
dominantBaseline="central"
|
||||||
|
fill={measurement.labelFill ?? palette.measurementStroke}
|
||||||
|
fillOpacity={
|
||||||
|
measurement.isSelected
|
||||||
|
? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
|
||||||
|
: FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
|
||||||
|
}
|
||||||
|
fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
||||||
|
fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
|
||||||
|
fontWeight="600"
|
||||||
|
textAnchor="middle"
|
||||||
|
transform={`rotate(${measurement.labelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
|
||||||
|
x={measurement.labelX}
|
||||||
|
y={measurement.labelY}
|
||||||
|
>
|
||||||
|
{measurement.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { memo } from 'react'
|
||||||
|
import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
|
||||||
|
import { toSvgX, toSvgY } from '../svg-paths'
|
||||||
|
|
||||||
|
type FloorplanLineSegment = {
|
||||||
|
start: Point2D
|
||||||
|
end: Point2D
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanRoofSegmentEntry = {
|
||||||
|
segment: RoofSegmentNode
|
||||||
|
points: string
|
||||||
|
ridgeLine: FloorplanLineSegment | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanRoofEntry = {
|
||||||
|
roof: RoofNode
|
||||||
|
segments: FloorplanRoofSegmentEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanRoofLayerProps = {
|
||||||
|
highlightedIdSet: ReadonlySet<string>
|
||||||
|
roofEntries: FloorplanRoofEntry[]
|
||||||
|
selectedIdSet: ReadonlySet<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
|
||||||
|
highlightedIdSet,
|
||||||
|
roofEntries,
|
||||||
|
selectedIdSet,
|
||||||
|
}: FloorplanRoofLayerProps) {
|
||||||
|
if (roofEntries.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{roofEntries.map(({ roof, segments }) => {
|
||||||
|
const roofSelected = selectedIdSet.has(roof.id)
|
||||||
|
const roofHighlighted = highlightedIdSet.has(roof.id)
|
||||||
|
const hasSelectedSegment = segments.some(({ segment }) => selectedIdSet.has(segment.id))
|
||||||
|
const hasHighlightedSegment = segments.some(({ segment }) =>
|
||||||
|
highlightedIdSet.has(segment.id),
|
||||||
|
)
|
||||||
|
const isRoofActive =
|
||||||
|
roofSelected || roofHighlighted || hasSelectedSegment || hasHighlightedSegment
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={roof.id} pointerEvents="none">
|
||||||
|
{segments.map(({ points, ridgeLine, segment }) => {
|
||||||
|
const isSegmentSelected = selectedIdSet.has(segment.id)
|
||||||
|
const isSegmentHighlighted = highlightedIdSet.has(segment.id)
|
||||||
|
const isSegmentActive = isSegmentSelected || isSegmentHighlighted
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={segment.id}>
|
||||||
|
<polygon
|
||||||
|
fill={
|
||||||
|
isSegmentActive
|
||||||
|
? 'rgba(14, 165, 233, 0.2)'
|
||||||
|
: isRoofActive
|
||||||
|
? 'rgba(14, 165, 233, 0.14)'
|
||||||
|
: 'rgba(14, 165, 233, 0.08)'
|
||||||
|
}
|
||||||
|
points={points}
|
||||||
|
stroke={
|
||||||
|
isSegmentActive
|
||||||
|
? '#0369a1'
|
||||||
|
: isRoofActive
|
||||||
|
? '#0ea5e9'
|
||||||
|
: 'rgba(14, 165, 233, 0.65)'
|
||||||
|
}
|
||||||
|
strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{ridgeLine ? (
|
||||||
|
<line
|
||||||
|
fill="none"
|
||||||
|
stroke={isSegmentActive ? '#0f172a' : 'rgba(3, 105, 161, 0.75)'}
|
||||||
|
strokeWidth={isSegmentActive ? '2' : '1.4'}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
x1={toSvgX(ridgeLine.start.x)}
|
||||||
|
x2={toSvgX(ridgeLine.end.x)}
|
||||||
|
y1={toSvgY(ridgeLine.start.y)}
|
||||||
|
y2={toSvgY(ridgeLine.end.y)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Point2D, StairNode, StairSegmentNode } from '@pascal-app/core'
|
||||||
|
import { memo, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react'
|
||||||
|
import {
|
||||||
|
buildSvgAnnularSectorPath,
|
||||||
|
buildSvgArcPath,
|
||||||
|
buildSvgArrowHeadPoints,
|
||||||
|
formatSvgPolygonPoints,
|
||||||
|
getArcPlanPoint,
|
||||||
|
toSvgX,
|
||||||
|
toSvgY,
|
||||||
|
} from '../svg-paths'
|
||||||
|
|
||||||
|
type FloorplanPolygonEntry = {
|
||||||
|
points: string
|
||||||
|
polygon: Point2D[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanStairSegmentEntry = {
|
||||||
|
segment: StairSegmentNode
|
||||||
|
points: string
|
||||||
|
treadBars: FloorplanPolygonEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanStairArrowEntry = {
|
||||||
|
head: Point2D[]
|
||||||
|
polyline: Point2D[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanStairEntry = {
|
||||||
|
arrow: FloorplanStairArrowEntry | null
|
||||||
|
hitPolygons: Point2D[][]
|
||||||
|
stair: StairNode
|
||||||
|
segments: FloorplanStairSegmentEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanPalette = {
|
||||||
|
deleteFill: string
|
||||||
|
deleteStroke: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FloorplanStairLayerProps = {
|
||||||
|
canFocusStairs: boolean
|
||||||
|
canSelectStairs: boolean
|
||||||
|
cursor: string
|
||||||
|
highlightedIdSet: ReadonlySet<string>
|
||||||
|
hitStrokeWidth: number
|
||||||
|
hoveredStairId: StairNode['id'] | null
|
||||||
|
isDeleteMode: boolean
|
||||||
|
onStairDoubleClick: (stair: StairNode, event: ReactMouseEvent<SVGElement>) => void
|
||||||
|
onStairHoverChange: (stairId: StairNode['id'] | null) => void
|
||||||
|
onStairHoverEnter: (stairId: StairNode['id']) => void
|
||||||
|
onStairPointerDown: (stairId: StairNode['id'], event: ReactPointerEvent<SVGElement>) => void
|
||||||
|
onStairSelect: (stairId: StairNode['id'], event: ReactMouseEvent<SVGElement>) => void
|
||||||
|
palette: FloorplanPalette
|
||||||
|
selectedIdSet: ReadonlySet<string>
|
||||||
|
stairEntries: FloorplanStairEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(max, Math.max(min, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
|
||||||
|
const stairType = stair.stairType ?? 'straight'
|
||||||
|
const baseSweepAngle =
|
||||||
|
stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
|
||||||
|
|
||||||
|
if (Math.abs(baseSweepAngle) >= Math.PI * 2) {
|
||||||
|
return Math.sign(baseSweepAngle || 1) * (Math.PI * 2 - 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSweepAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
||||||
|
canFocusStairs,
|
||||||
|
canSelectStairs,
|
||||||
|
cursor,
|
||||||
|
highlightedIdSet,
|
||||||
|
hitStrokeWidth,
|
||||||
|
hoveredStairId,
|
||||||
|
isDeleteMode,
|
||||||
|
onStairDoubleClick,
|
||||||
|
onStairHoverChange,
|
||||||
|
onStairHoverEnter,
|
||||||
|
onStairPointerDown,
|
||||||
|
onStairSelect,
|
||||||
|
palette,
|
||||||
|
selectedIdSet,
|
||||||
|
stairEntries,
|
||||||
|
}: FloorplanStairLayerProps) {
|
||||||
|
if (stairEntries.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{stairEntries.map(({ arrow, hitPolygons, stair, segments }) => {
|
||||||
|
const stairSelected = selectedIdSet.has(stair.id)
|
||||||
|
const stairHighlighted = highlightedIdSet.has(stair.id)
|
||||||
|
const segmentSelected = segments.some(({ segment }) => selectedIdSet.has(segment.id))
|
||||||
|
const segmentHighlighted = segments.some(({ segment }) => highlightedIdSet.has(segment.id))
|
||||||
|
const isHovered = hoveredStairId === stair.id
|
||||||
|
const isDeleteHovered = isDeleteMode && isHovered
|
||||||
|
const isSelectionActive =
|
||||||
|
stairSelected || stairHighlighted || segmentSelected || segmentHighlighted
|
||||||
|
const stairType = stair.stairType ?? 'straight'
|
||||||
|
const normalizedSweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
|
||||||
|
const sectorStartAngle = stair.rotation - normalizedSweepAngle / 2
|
||||||
|
const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
|
||||||
|
const stairCenter = {
|
||||||
|
x: stair.position[0],
|
||||||
|
y: stair.position[2],
|
||||||
|
}
|
||||||
|
const innerRadius = Math.max(
|
||||||
|
stairType === 'spiral' ? 0.05 : 0.2,
|
||||||
|
stair.innerRadius ?? (stairType === 'spiral' ? 0.2 : 0.9),
|
||||||
|
)
|
||||||
|
const outerRadius = innerRadius + stair.width
|
||||||
|
const centerlineRadius = innerRadius + stair.width / 2
|
||||||
|
const curvedStroke = isDeleteHovered
|
||||||
|
? palette.deleteStroke
|
||||||
|
: isSelectionActive
|
||||||
|
? '#2563eb'
|
||||||
|
: 'rgba(31, 41, 55, 0.9)'
|
||||||
|
const curvedAccent = isDeleteHovered
|
||||||
|
? palette.deleteStroke
|
||||||
|
: isSelectionActive
|
||||||
|
? '#1d4ed8'
|
||||||
|
: 'rgba(23, 23, 23, 0.96)'
|
||||||
|
const curvedFill = isDeleteHovered
|
||||||
|
? palette.deleteFill
|
||||||
|
: isSelectionActive
|
||||||
|
? 'rgba(59, 130, 246, 0.16)'
|
||||||
|
: '#ffffff'
|
||||||
|
const straightAccent = isDeleteHovered
|
||||||
|
? palette.deleteStroke
|
||||||
|
: isSelectionActive
|
||||||
|
? '#1d4ed8'
|
||||||
|
: 'rgba(23, 23, 23, 0.96)'
|
||||||
|
const straightStroke = isDeleteHovered
|
||||||
|
? palette.deleteStroke
|
||||||
|
: isSelectionActive
|
||||||
|
? '#1d4ed8'
|
||||||
|
: 'rgba(23, 23, 23, 0.88)'
|
||||||
|
const straightTread = isDeleteHovered
|
||||||
|
? palette.deleteStroke
|
||||||
|
: isSelectionActive
|
||||||
|
? 'rgba(37, 99, 235, 0.78)'
|
||||||
|
: 'rgba(38, 38, 38, 0.62)'
|
||||||
|
const straightFill = isDeleteHovered
|
||||||
|
? palette.deleteFill
|
||||||
|
: isSelectionActive
|
||||||
|
? 'rgba(59, 130, 246, 0.08)'
|
||||||
|
: 'rgba(255, 255, 255, 0.02)'
|
||||||
|
const curvedOuterLineWidth = isSelectionActive ? '2' : '1.4'
|
||||||
|
const curvedInnerLineWidth = isSelectionActive ? '1.7' : '1.2'
|
||||||
|
const stairSymbol =
|
||||||
|
stairType === 'spiral' ? (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d={buildSvgAnnularSectorPath(
|
||||||
|
stairCenter,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
sectorStartAngle,
|
||||||
|
sectorEndAngle,
|
||||||
|
)}
|
||||||
|
fill={curvedFill}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={buildSvgArcPath(stairCenter, outerRadius, sectorStartAngle, sectorEndAngle)}
|
||||||
|
fill="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedStroke}
|
||||||
|
strokeWidth={curvedOuterLineWidth}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={buildSvgArcPath(stairCenter, innerRadius, sectorStartAngle, sectorEndAngle)}
|
||||||
|
fill="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedStroke}
|
||||||
|
strokeWidth={curvedInnerLineWidth}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{Array.from({ length: Math.max(6, stair.stepCount) }, (_, index) => {
|
||||||
|
const stepCount = Math.max(6, stair.stepCount)
|
||||||
|
const stepSweep = normalizedSweepAngle / stepCount
|
||||||
|
const angle = sectorStartAngle + stepSweep * index
|
||||||
|
const innerPoint = getArcPlanPoint(stairCenter, innerRadius, angle)
|
||||||
|
const outerPoint = getArcPlanPoint(stairCenter, outerRadius, angle)
|
||||||
|
const dashedFromIndex = Math.floor(stepCount * 0.68)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={`${stair.id}:spiral-step:${index}`}
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={index === stepCount - 1 ? curvedAccent : curvedStroke}
|
||||||
|
strokeDasharray={index >= dashedFromIndex ? '0.1 0.08' : undefined}
|
||||||
|
strokeWidth={index === stepCount - 1 ? '1.8' : '1.15'}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
x1={toSvgX(innerPoint.x)}
|
||||||
|
x2={toSvgX(outerPoint.x)}
|
||||||
|
y1={toSvgY(innerPoint.y)}
|
||||||
|
y2={toSvgY(outerPoint.y)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<circle
|
||||||
|
cx={toSvgX(stairCenter.x)}
|
||||||
|
cy={toSvgY(stairCenter.y)}
|
||||||
|
fill="#ffffff"
|
||||||
|
pointerEvents="none"
|
||||||
|
r={Math.max(innerRadius * 0.18, 0.06)}
|
||||||
|
stroke={curvedAccent}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{(() => {
|
||||||
|
const directionAngle = sectorStartAngle + normalizedSweepAngle * 0.86
|
||||||
|
const arrowPoint = getArcPlanPoint(stairCenter, centerlineRadius, directionAngle)
|
||||||
|
const tangentAngle =
|
||||||
|
directionAngle + (normalizedSweepAngle >= 0 ? Math.PI / 2 : -Math.PI / 2)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<polygon
|
||||||
|
fill={curvedAccent}
|
||||||
|
key={`${stair.id}:spiral-arrow`}
|
||||||
|
pointerEvents="none"
|
||||||
|
points={buildSvgArrowHeadPoints(
|
||||||
|
arrowPoint,
|
||||||
|
tangentAngle,
|
||||||
|
clamp(stair.width * 0.18, 0.12, 0.18),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
|
) : stairType === 'curved' ? (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d={buildSvgAnnularSectorPath(
|
||||||
|
stairCenter,
|
||||||
|
innerRadius,
|
||||||
|
outerRadius,
|
||||||
|
sectorStartAngle,
|
||||||
|
sectorEndAngle,
|
||||||
|
)}
|
||||||
|
fill={curvedFill}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={buildSvgArcPath(stairCenter, outerRadius, sectorStartAngle, sectorEndAngle)}
|
||||||
|
fill="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedStroke}
|
||||||
|
strokeWidth={curvedOuterLineWidth}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={buildSvgArcPath(stairCenter, innerRadius, sectorStartAngle, sectorEndAngle)}
|
||||||
|
fill="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedStroke}
|
||||||
|
strokeWidth={curvedInnerLineWidth}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{Array.from({ length: Math.max(4, stair.stepCount) + 1 }, (_, index) => {
|
||||||
|
const stepCount = Math.max(4, stair.stepCount)
|
||||||
|
const stepSweep = normalizedSweepAngle / stepCount
|
||||||
|
const angle = sectorStartAngle + stepSweep * index
|
||||||
|
const innerPoint = getArcPlanPoint(stairCenter, innerRadius, angle)
|
||||||
|
const outerPoint = getArcPlanPoint(stairCenter, outerRadius, angle)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={`${stair.id}:curved-step:${index}`}
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedStroke}
|
||||||
|
strokeWidth={index === 0 || index === stepCount ? '1.5' : '1.1'}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
x1={toSvgX(innerPoint.x)}
|
||||||
|
x2={toSvgX(outerPoint.x)}
|
||||||
|
y1={toSvgY(innerPoint.y)}
|
||||||
|
y2={toSvgY(outerPoint.y)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<path
|
||||||
|
d={buildSvgArcPath(
|
||||||
|
stairCenter,
|
||||||
|
centerlineRadius,
|
||||||
|
sectorStartAngle + (normalizedSweepAngle / Math.max(4, stair.stepCount)) * 0.55,
|
||||||
|
sectorEndAngle - (normalizedSweepAngle / Math.max(4, stair.stepCount)) * 0.55,
|
||||||
|
)}
|
||||||
|
fill="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={curvedAccent}
|
||||||
|
strokeDasharray="0.08 0.11"
|
||||||
|
strokeWidth="1.1"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{(() => {
|
||||||
|
const stepCount = Math.max(4, stair.stepCount)
|
||||||
|
const stepSweep = normalizedSweepAngle / stepCount
|
||||||
|
const arrowAngle = sectorEndAngle - stepSweep * 0.8
|
||||||
|
const arrowPoint = getArcPlanPoint(stairCenter, centerlineRadius, arrowAngle)
|
||||||
|
const tangentAngle =
|
||||||
|
arrowAngle + (normalizedSweepAngle >= 0 ? Math.PI / 2 : -Math.PI / 2)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<polygon
|
||||||
|
fill={curvedAccent}
|
||||||
|
key={`${stair.id}:curved-arrow`}
|
||||||
|
pointerEvents="none"
|
||||||
|
points={buildSvgArrowHeadPoints(
|
||||||
|
arrowPoint,
|
||||||
|
tangentAngle,
|
||||||
|
clamp(stair.width * 0.16, 0.1, 0.16),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{segments.map(({ points, segment, treadBars }) => (
|
||||||
|
<g key={segment.id}>
|
||||||
|
<polygon
|
||||||
|
fill={straightFill}
|
||||||
|
pointerEvents="none"
|
||||||
|
points={points}
|
||||||
|
stroke={straightStroke}
|
||||||
|
strokeWidth={isSelectionActive ? '2' : '1.35'}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
{treadBars.map((treadBar, treadIndex) => (
|
||||||
|
<polygon
|
||||||
|
fill={straightTread}
|
||||||
|
key={`${segment.id}:tread:${treadIndex}`}
|
||||||
|
pointerEvents="none"
|
||||||
|
points={segment.segmentType === 'landing' ? '' : treadBar.points}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
{arrow?.polyline && arrow.polyline.length >= 2 ? (
|
||||||
|
<>
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
points={formatSvgPolygonPoints(arrow.polyline)}
|
||||||
|
pointerEvents="none"
|
||||||
|
stroke={straightAccent}
|
||||||
|
strokeWidth="1.15"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={toSvgX(arrow.polyline[0]!.x)}
|
||||||
|
cy={toSvgY(arrow.polyline[0]!.y)}
|
||||||
|
fill={straightAccent}
|
||||||
|
pointerEvents="none"
|
||||||
|
r="0.045"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
fill={straightAccent}
|
||||||
|
points={formatSvgPolygonPoints(arrow.head)}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
key={stair.id}
|
||||||
|
onClick={
|
||||||
|
canSelectStairs
|
||||||
|
? (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onStairSelect(stair.id, event)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDoubleClick={
|
||||||
|
canFocusStairs
|
||||||
|
? (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onStairDoubleClick(stair, event)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
|
||||||
|
onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
|
||||||
|
onPointerDown={
|
||||||
|
canFocusStairs && stairSelected
|
||||||
|
? (event) => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
onStairPointerDown(stair.id, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
pointerEvents={canSelectStairs ? undefined : 'none'}
|
||||||
|
style={canSelectStairs ? { cursor } : undefined}
|
||||||
|
>
|
||||||
|
{hitPolygons.map((polygon, polygonIndex) => (
|
||||||
|
<polygon
|
||||||
|
fill="transparent"
|
||||||
|
key={`${stair.id}:hit:${polygonIndex}`}
|
||||||
|
points={formatSvgPolygonPoints(polygon)}
|
||||||
|
pointerEvents={canSelectStairs ? 'all' : 'none'}
|
||||||
|
stroke="transparent"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={hitStrokeWidth}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<title>{stair.name || 'Staircase'}</title>
|
||||||
|
{stairSymbol}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Point2D } from '@pascal-app/core'
|
||||||
|
|
||||||
|
function toSvgX(value: number) {
|
||||||
|
return -value
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSvgY(value: number) {
|
||||||
|
return -value
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSvgPoint(point: Point2D) {
|
||||||
|
return {
|
||||||
|
x: toSvgX(point.x),
|
||||||
|
y: toSvgY(point.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPolygonPath(points: Point2D[], holes: Point2D[][] = []) {
|
||||||
|
const formatSubpath = (subpathPoints: Point2D[]) => {
|
||||||
|
const [firstPoint, ...restPoints] = subpathPoints
|
||||||
|
if (!firstPoint) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstSvgPoint = toSvgPoint(firstPoint)
|
||||||
|
|
||||||
|
return [
|
||||||
|
`M ${firstSvgPoint.x} ${firstSvgPoint.y}`,
|
||||||
|
...restPoints.map((point) => {
|
||||||
|
const svgPoint = toSvgPoint(point)
|
||||||
|
return `L ${svgPoint.x} ${svgPoint.y}`
|
||||||
|
}),
|
||||||
|
'Z',
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [points, ...holes].map(formatSubpath).filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSvgPolylinePath(points: Point2D[]) {
|
||||||
|
if (points.length < 2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
.map((point, index) => {
|
||||||
|
const svgPoint = toSvgPoint(point)
|
||||||
|
return `${index === 0 ? 'M' : 'L'} ${svgPoint.x} ${svgPoint.y}`
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArcPlanPoint(center: Point2D, radius: number, angle: number): Point2D {
|
||||||
|
return {
|
||||||
|
x: center.x + Math.cos(angle) * radius,
|
||||||
|
y: center.y + Math.sin(angle) * radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSvgArcPath(
|
||||||
|
center: Point2D,
|
||||||
|
radius: number,
|
||||||
|
startAngle: number,
|
||||||
|
endAngle: number,
|
||||||
|
) {
|
||||||
|
const start = getArcPlanPoint(center, radius, startAngle)
|
||||||
|
const end = getArcPlanPoint(center, radius, endAngle)
|
||||||
|
const delta = endAngle - startAngle
|
||||||
|
const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
|
||||||
|
const sweepFlag = delta >= 0 ? 1 : 0
|
||||||
|
|
||||||
|
return `M ${toSvgX(start.x)} ${toSvgY(start.y)} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${toSvgX(end.x)} ${toSvgY(end.y)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSvgAnnularSectorPath(
|
||||||
|
center: Point2D,
|
||||||
|
innerRadius: number,
|
||||||
|
outerRadius: number,
|
||||||
|
startAngle: number,
|
||||||
|
endAngle: number,
|
||||||
|
) {
|
||||||
|
const outerStart = getArcPlanPoint(center, outerRadius, startAngle)
|
||||||
|
const outerEnd = getArcPlanPoint(center, outerRadius, endAngle)
|
||||||
|
const innerEnd = getArcPlanPoint(center, innerRadius, endAngle)
|
||||||
|
const innerStart = getArcPlanPoint(center, innerRadius, startAngle)
|
||||||
|
const delta = endAngle - startAngle
|
||||||
|
const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
|
||||||
|
const sweepFlag = delta >= 0 ? 1 : 0
|
||||||
|
const reverseSweepFlag = sweepFlag ? 0 : 1
|
||||||
|
|
||||||
|
return [
|
||||||
|
`M ${toSvgX(outerStart.x)} ${toSvgY(outerStart.y)}`,
|
||||||
|
`A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} ${sweepFlag} ${toSvgX(outerEnd.x)} ${toSvgY(outerEnd.y)}`,
|
||||||
|
`L ${toSvgX(innerEnd.x)} ${toSvgY(innerEnd.y)}`,
|
||||||
|
`A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} ${reverseSweepFlag} ${toSvgX(innerStart.x)} ${toSvgY(innerStart.y)}`,
|
||||||
|
'Z',
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSvgPolygonPoints(points: Point2D[]) {
|
||||||
|
return points
|
||||||
|
.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`)
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: number) {
|
||||||
|
const left = {
|
||||||
|
x: point.x - size * Math.cos(angle - Math.PI / 6),
|
||||||
|
y: point.y - size * Math.sin(angle - Math.PI / 6),
|
||||||
|
}
|
||||||
|
const right = {
|
||||||
|
x: point.x - size * Math.cos(angle + Math.PI / 6),
|
||||||
|
y: point.y - size * Math.sin(angle + Math.PI / 6),
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatSvgPolygonPoints([point, left, right])
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toSvgPoint, toSvgX, toSvgY }
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import type { Point2D } from '../systems/wall/wall-mitering'
|
import type { Point2D } from '@pascal-app/core'
|
||||||
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
|
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
|
||||||
|
|
||||||
export function clampPlanValue(value: number, min: number, max: number) {
|
export function clampPlanValue(value: number, min: number, max: number) {
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export {
|
||||||
|
clampPlanValue,
|
||||||
|
doesPolygonIntersectSelectionBounds,
|
||||||
|
getDistanceToWallSegment,
|
||||||
|
getFloorplanSelectionBounds,
|
||||||
|
getPlanPointDistance,
|
||||||
|
getRotatedRectanglePolygon,
|
||||||
|
getThickPlanLinePolygon,
|
||||||
|
interpolatePlanPoint,
|
||||||
|
isPointInsidePolygon,
|
||||||
|
isPointInsidePolygonWithHoles,
|
||||||
|
isPointInsideSelectionBounds,
|
||||||
|
movePlanPointTowards,
|
||||||
|
pointMatchesWallPlanPoint,
|
||||||
|
rotatePlanVector,
|
||||||
|
} from './geometry'
|
||||||
|
export {
|
||||||
|
buildFloorplanItemEntry,
|
||||||
|
collectLevelDescendants,
|
||||||
|
getItemFloorplanTransform,
|
||||||
|
} from './items'
|
||||||
|
export {
|
||||||
|
buildFloorplanStairEntry,
|
||||||
|
computeFloorplanStairSegmentTransforms,
|
||||||
|
getFloorplanStairSegmentPolygon,
|
||||||
|
} from './stairs'
|
||||||
|
export type {
|
||||||
|
FloorplanItemEntry,
|
||||||
|
FloorplanLineSegment,
|
||||||
|
FloorplanNodeTransform,
|
||||||
|
FloorplanSelectionBounds,
|
||||||
|
FloorplanStairArrowEntry,
|
||||||
|
FloorplanStairEntry,
|
||||||
|
FloorplanStairSegmentEntry,
|
||||||
|
LevelDescendantMap,
|
||||||
|
StairSegmentTransform,
|
||||||
|
} from './types'
|
||||||
|
export { getFloorplanWall, getFloorplanWallThickness } from './walls'
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { getScaledDimensions } from '../schema'
|
import {
|
||||||
import type { AnyNode, AnyNodeId, ItemNode, LevelNode } from '../schema'
|
getScaledDimensions,
|
||||||
import useLiveTransforms from '../store/use-live-transforms'
|
type AnyNode,
|
||||||
|
type AnyNodeId,
|
||||||
|
type ItemNode,
|
||||||
|
type LevelNode,
|
||||||
|
useLiveTransforms,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { getRotatedRectanglePolygon, rotatePlanVector } from './geometry'
|
import { getRotatedRectanglePolygon, rotatePlanVector } from './geometry'
|
||||||
import type { FloorplanItemEntry, FloorplanNodeTransform, LevelDescendantMap } from './types'
|
import type { FloorplanItemEntry, FloorplanNodeTransform, LevelDescendantMap } from './types'
|
||||||
|
|
||||||
+3
-3
@@ -2,6 +2,7 @@ import type {
|
|||||||
CeilingNode,
|
CeilingNode,
|
||||||
DoorNode,
|
DoorNode,
|
||||||
ItemNode,
|
ItemNode,
|
||||||
|
Point2D,
|
||||||
RoofNode,
|
RoofNode,
|
||||||
RoofSegmentNode,
|
RoofSegmentNode,
|
||||||
SlabNode,
|
SlabNode,
|
||||||
@@ -14,9 +15,8 @@ import {
|
|||||||
getDistanceToWallSegment,
|
getDistanceToWallSegment,
|
||||||
isPointInsidePolygon,
|
isPointInsidePolygon,
|
||||||
isPointInsidePolygonWithHoles,
|
isPointInsidePolygonWithHoles,
|
||||||
type FloorplanSelectionBounds,
|
} from './geometry'
|
||||||
type Point2D,
|
import type { FloorplanSelectionBounds } from './types'
|
||||||
} from '@pascal-app/core'
|
|
||||||
|
|
||||||
type OpeningNode = WindowNode | DoorNode
|
type OpeningNode = WindowNode | DoorNode
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { StairNode, StairSegmentNode } from '../schema'
|
import type { Point2D, StairNode, StairSegmentNode } from '@pascal-app/core'
|
||||||
import type { Point2D } from '../systems/wall/wall-mitering'
|
|
||||||
import {
|
import {
|
||||||
clampPlanValue,
|
clampPlanValue,
|
||||||
getPlanPointDistance,
|
getPlanPointDistance,
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AnyNode, ItemNode, StairNode, StairSegmentNode } from '../schema'
|
import type { AnyNode, ItemNode, Point2D, StairNode, StairSegmentNode } from '@pascal-app/core'
|
||||||
import type { Point2D } from '../systems/wall/wall-mitering'
|
|
||||||
|
|
||||||
export type FloorplanNodeTransform = {
|
export type FloorplanNodeTransform = {
|
||||||
position: Point2D
|
position: Point2D
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { WallNode } from '../schema'
|
import type { WallNode } from '@pascal-app/core'
|
||||||
|
|
||||||
const FLOORPLAN_WALL_THICKNESS_SCALE = 1.18
|
const FLOORPLAN_WALL_THICKNESS_SCALE = 1.18
|
||||||
const FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS = 0.13
|
const FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS = 0.13
|
||||||
Reference in New Issue
Block a user