Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions packages/editor/src/components/editor/floorplan-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5165,8 +5165,82 @@ function FloorplanLinearDraftLayer({
}
}, [isWallBuildActive, metricNotation, unit, wallDraftEnd, wallDraftStart, walls])

// Axis guides for wall and fence drafts — parity with the 3D tools'
// `DraftAxisGuides`: an X/Z cross through the draft start, and a single
// long line PERPENDICULAR to the segment through the moving endpoint (a
// second cross would collide with the start cross on axis-aligned
// segments). Long solid lines in world space — cheap for the SVG renderer,
// unlike dashed strokes which tessellate per dash over 2000 m. Stroke
// width is budgeted in screen pixels via `unitsPerPixel`, matching the
// alignment-guide layer.
const draftAxisGuideLines = useMemo(() => {
const lines: Array<{ x1: number; y1: number; x2: number; y2: number }> = []
const pushCross = (point: WallPlanPoint) => {
lines.push(
{
x1: point[0] - DRAFT_AXIS_GUIDE_EXTENT,
y1: point[1],
x2: point[0] + DRAFT_AXIS_GUIDE_EXTENT,
y2: point[1],
},
{
x1: point[0],
y1: point[1] - DRAFT_AXIS_GUIDE_EXTENT,
x2: point[0],
y2: point[1] + DRAFT_AXIS_GUIDE_EXTENT,
},
)
}
const pushDraft = (start: WallPlanPoint, end: WallPlanPoint) => {
pushCross(start)
const dx = end[0] - start[0]
const dy = end[1] - start[1]
const length = Math.hypot(dx, dy)
if (length < 1e-6) return
const nx = -dy / length
const ny = dx / length
lines.push({
x1: end[0] - nx * DRAFT_AXIS_GUIDE_EXTENT,
y1: end[1] - ny * DRAFT_AXIS_GUIDE_EXTENT,
x2: end[0] + nx * DRAFT_AXIS_GUIDE_EXTENT,
y2: end[1] + ny * DRAFT_AXIS_GUIDE_EXTENT,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2D endpoint guide length mismatch

Medium Severity

The 2D draft axis guide draws its endpoint perpendicular line for segments as short as 1e-6 m. This is inconsistent with 3D DraftAxisGuides and wall previews, which require segments of at least 0.01 m, leading to a misleading 2D guide for very short drafts without a corresponding 3D visual.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 879bf6e. Configure here.

}
if (isWallBuildActive && wallDraftStart && wallDraftEnd) {
pushDraft(wallDraftStart, wallDraftEnd)
}
if (isFenceBuildActive && fenceDraftStart && fenceDraftEnd) {
pushDraft(fenceDraftStart, fenceDraftEnd)
}
return lines.length > 0 ? lines : null
}, [
isFenceBuildActive,
isWallBuildActive,
fenceDraftEnd,
fenceDraftStart,
wallDraftEnd,
wallDraftStart,
])

return (
<>
{draftAxisGuideLines && (
<g pointerEvents="none">
{draftAxisGuideLines.map((line, index) => (
<line
key={index}
stroke="#818cf8"
strokeOpacity={0.5}
strokeWidth={unitsPerPixel}
x1={line.x1}
x2={line.x2}
y1={line.y1}
y2={line.y2}
/>
))}
</g>
)}

<FloorplanDraftLayer
anchorFill={draftStroke}
draftAnchorPoints={EMPTY_DRAFT_ANCHOR_POINTS}
Expand Down Expand Up @@ -5195,6 +5269,8 @@ function FloorplanLinearDraftLayer({
}

const EMPTY_DRAFT_ANCHOR_POINTS: Array<{ x: number; y: number; isPrimary: boolean }> = []
/** World-space half-length of the 2D draft axis guide lines (matches the 3D tools' 2000 m guides). */
const DRAFT_AXIS_GUIDE_EXTENT = 1000

export function FloorplanPanel({
/**
Expand Down
120 changes: 38 additions & 82 deletions packages/nodes/src/fence/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import {
} from '@pascal-app/editor'

import { getSceneTheme, useViewer } from '@pascal-app/viewer'
import { Html } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
Expand All @@ -60,6 +59,14 @@ import {
type Mesh,
Vector3,
} from 'three'
import {
DraftAngleArc,
type DraftAngleLabel,
type DraftAxisGuideState,
DraftAxisGuides,
DraftMeasurementLabel,
getNearestAxisAngleLabel,
} from '../shared/draft-axis-guides'

const FENCE_PREVIEW_HEIGHT = 1.8
const FENCE_PREVIEW_THICKNESS = 0.08
Expand All @@ -86,20 +93,6 @@ const DRAFT_ANGLE_LABEL_Y_OFFSET = 0.08
const DRAFT_ANGLE_ARC_Y_OFFSET = 0.012
const DRAFT_ANGLE_ARC_MIN_RADIUS = 0.32
const DRAFT_ANGLE_ARC_MAX_RADIUS = 0.72
const DRAFT_ANGLE_ARC_SEGMENTS = 24

type DraftAngleLabel = {
id: string
label: string
position: [number, number, number]
arc: {
center: FencePlanPoint
radius: number
startAngle: number
endAngle: number
y: number
}
}

type DraftMeasurementState = {
lengthLabel: string
Expand Down Expand Up @@ -498,6 +491,7 @@ const StraightFenceTool: React.FC = () => {
const endingPoint = useRef(new Vector3(0, 0, 0))
const buildingState = useRef(0)
const [draftMeasurement, setDraftMeasurement] = useState<DraftMeasurementState>(null)
const [axisGuide, setAxisGuide] = useState<DraftAxisGuideState>(null)
const measurementColor = isDark ? '#ffffff' : '#111111'
const measurementShadowColor = isDark ? '#111111' : '#ffffff'

Expand Down Expand Up @@ -544,6 +538,7 @@ const StraightFenceTool: React.FC = () => {
buildingState.current = 0
previewRef.current.visible = false
setDraftMeasurement(null)
setAxisGuide(null)
const draftPreview = useFloorplanDraftPreview.getState()
draftPreview.setFenceDraftStart(null)
draftPreview.setFenceDraftEnd(null)
Expand Down Expand Up @@ -590,6 +585,16 @@ const StraightFenceTool: React.FC = () => {
draftPreview.setFenceDraftStart([startingPoint.current.x, startingPoint.current.z])
draftPreview.setFenceDraftEnd(snappedLocal)
cursorRef.current.position.copy(endingPoint.current)
setAxisGuide({
origin: [startingPoint.current.x, startingPoint.current.z],
endOrigin: snappedLocal,
y: startingPoint.current.y,
angleLabel: getNearestAxisAngleLabel(
[startingPoint.current.x, startingPoint.current.z],
snappedLocal,
startingPoint.current.y,
),
})
const currentFenceEnd: FencePlanPoint = [snappedLocal[0], snappedLocal[1]]
if (
previousFenceEnd &&
Expand Down Expand Up @@ -627,6 +632,7 @@ const StraightFenceTool: React.FC = () => {
)
cursorRef.current.position.set(snappedPoint[0], event.localPosition[1], snappedPoint[1])
setDraftMeasurement(null)
setAxisGuide(null)
}
}

Expand Down Expand Up @@ -658,6 +664,12 @@ const StraightFenceTool: React.FC = () => {
triggerSFX('sfx:structure-build-start')
previewRef.current.visible = true
setDraftMeasurement(null)
setAxisGuide({
origin: snappedStart,
endOrigin: null,
y: event.localPosition[1],
angleLabel: null,
})
} else {
const angleLocked = isAngleSnapActive()
const snappedEnd = alignPoint(
Expand Down Expand Up @@ -709,6 +721,12 @@ const StraightFenceTool: React.FC = () => {
previewRef.current.visible = false
buildingState.current = 1
setDraftMeasurement(null)
setAxisGuide({
origin: nextStart,
endOrigin: null,
y: event.localPosition[1],
angleLabel: null,
})
}
}

Expand Down Expand Up @@ -738,6 +756,11 @@ const StraightFenceTool: React.FC = () => {

return (
<group>
<DraftAxisGuides
guide={axisGuide}
labelColor={measurementColor}
labelShadowColor={measurementShadowColor}
/>
<CursorSphere height={previewHeight} ref={cursorRef} />
<mesh layers={EDITOR_LAYER} ref={previewRef} renderOrder={1} visible={false}>
<shapeGeometry />
Expand Down Expand Up @@ -927,71 +950,4 @@ const SplineFenceDraft: React.FC = () => {
)
}

function DraftAngleArc({ arc, color }: { arc: DraftAngleLabel['arc']; color: string }) {
const geometry = useMemo(() => {
const segmentCount = Math.max(
8,
Math.ceil((Math.abs(arc.endAngle - arc.startAngle) / Math.PI) * DRAFT_ANGLE_ARC_SEGMENTS),
)

const points = Array.from({ length: segmentCount + 1 }, (_, index) => {
const t = index / segmentCount
const angle = arc.startAngle + (arc.endAngle - arc.startAngle) * t

return new Vector3(
arc.center[0] + Math.cos(angle) * arc.radius,
arc.y,
arc.center[1] + Math.sin(angle) * arc.radius,
)
})

return new BufferGeometry().setFromPoints(points)
}, [arc])

return (
// @ts-expect-error - R3F accepts Three line primitives, matching the other editor drawing tools.
<line frustumCulled={false} geometry={geometry} layers={EDITOR_LAYER} renderOrder={2}>
<lineBasicNodeMaterial
color={color}
depthTest={false}
depthWrite={false}
linewidth={2}
opacity={0.95}
transparent
/>
</line>
)
}

function DraftMeasurementLabel({
color,
label,
position,
shadowColor,
}: {
color: string
label: string
position: [number, number, number]
shadowColor: string
}) {
return (
<Html
center
position={position}
style={{ pointerEvents: 'none', userSelect: 'none' }}
zIndexRange={[100, 0]}
>
<div
className="whitespace-nowrap font-bold font-mono text-[15px]"
style={{
color,
textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
}}
>
{label}
</div>
</Html>
)
}

export default FenceTool
Loading
Loading