Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3731eb3
Add roof surface placement support for items
sudhir9297 May 18, 2026
ed53bc2
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
fd8e02c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
7c1e383
fixed conflict
sudhir9297 May 20, 2026
b3377da
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
f177a65
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 22, 2026
9af7491
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 22, 2026
fd27524
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 27, 2026
b516298
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 28, 2026
ebfc8ce
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 3, 2026
b7b313b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 4, 2026
b2ad645
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 4, 2026
bffdb4a
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 8, 2026
ee7b10c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 9, 2026
7d4b474
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 10, 2026
3a3318c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 13, 2026
26df69f
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 17, 2026
5376e07
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 22, 2026
d2204aa
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 23, 2026
f2a5186
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 29, 2026
5841052
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 1, 2026
a6acaa3
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 8, 2026
e0fec5b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 10, 2026
7fa9276
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 13, 2026
c3ff9d6
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 14, 2026
00d84d5
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 19, 2026
2c2dabc
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 22, 2026
29f914f
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 22, 2026
1cbf910
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 23, 2026
8f59da7
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 24, 2026
321b710
fix(viewer): keep outlines during camera movement
sudhir9297 Jul 24, 2026
81d4832
fix(editor): prevent floorplan clipping during rotation
sudhir9297 Jul 24, 2026
800dfb7
fix(editor): keep compass rotation in sync
sudhir9297 Jul 24, 2026
dd98a76
fix(editor): stabilize floorplan and camera sync
sudhir9297 Jul 24, 2026
bc86bdd
fix floorplan registry scale subscriptions
sudhir9297 Jul 24, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,17 @@ describe('floorplan render context', () => {
expect(scale.read).toBe(read)
expect(read()).toBe(0.01)
})

test('notifies selected-handle renderers when a hidden floorplan becomes visible', () => {
const scale = createFloorplanRenderScaleReference(30)
let renderedCurveHandleRadius = 8 * scale.read()
const unsubscribe = scale.subscribe(() => {
renderedCurveHandleRadius = 8 * scale.read()
})

scale.update(0.02)

expect(renderedCurveHandleRadius).toBe(0.16)
unsubscribe()
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
'use client'

import type { FloorplanPalette } from '@pascal-app/core'
import { createContext, type ReactNode, useContext, useMemo, useRef } from 'react'
import {
createContext,
type ReactNode,
useContext,
useLayoutEffect,
useMemo,
useRef,
useSyncExternalStore,
} from 'react'

/**
* Per-frame render context shared between the legacy `floorplan-panel.tsx`
Expand Down Expand Up @@ -40,6 +48,7 @@ export type FloorplanStaticRenderContextValue = Omit<
> & {
getSceneRotationDeg: () => number
getUnitsPerPixel: () => number
subscribeUnitsPerPixel: (listener: () => void) => () => void
}

const FloorplanStaticRenderContext = createContext<FloorplanStaticRenderContextValue | null>(null)
Expand All @@ -48,17 +57,25 @@ const FloorplanUnitsPerPixelContext = createContext(1)

export type FloorplanRenderScaleReference = {
read: () => number
subscribe: (listener: () => void) => () => void
update: (unitsPerPixel: number) => void
}

export function createFloorplanRenderScaleReference(
initialUnitsPerPixel: number,
): FloorplanRenderScaleReference {
let unitsPerPixel = initialUnitsPerPixel
const listeners = new Set<() => void>()
return {
read: () => unitsPerPixel,
subscribe: (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
update: (nextUnitsPerPixel) => {
if (Object.is(unitsPerPixel, nextUnitsPerPixel)) return
unitsPerPixel = nextUnitsPerPixel
for (const listener of listeners) listener()
},
}
}
Expand All @@ -78,11 +95,20 @@ export function FloorplanRenderProvider({
if (!renderScaleReference.current) {
renderScaleReference.current = createFloorplanRenderScaleReference(unitsPerPixel)
}
renderScaleReference.current.update(unitsPerPixel)
const getUnitsPerPixel = renderScaleReference.current.read
const subscribeUnitsPerPixel = renderScaleReference.current.subscribe
useLayoutEffect(() => {
renderScaleReference.current?.update(unitsPerPixel)
}, [unitsPerPixel])
const staticValue = useMemo<FloorplanStaticRenderContextValue>(
() => ({ palette, hatchPatternId, getSceneRotationDeg, getUnitsPerPixel }),
[palette, hatchPatternId, getSceneRotationDeg, getUnitsPerPixel],
() => ({
palette,
hatchPatternId,
getSceneRotationDeg,
getUnitsPerPixel,
subscribeUnitsPerPixel,
}),
[palette, hatchPatternId, getSceneRotationDeg, getUnitsPerPixel, subscribeUnitsPerPixel],
)
return (
<FloorplanStaticRenderContext.Provider value={staticValue}>
Expand Down Expand Up @@ -124,6 +150,18 @@ export function useFloorplanStaticRender(): FloorplanStaticRenderContextValue |
return useContext(FloorplanStaticRenderContext)
}

const subscribeToNoFloorplanScale = () => () => {}
const readDefaultFloorplanScale = () => 1

export function useFloorplanStaticUnitsPerPixel(): number {
const staticValue = useContext(FloorplanStaticRenderContext)
return useSyncExternalStore(
staticValue?.subscribeUnitsPerPixel ?? subscribeToNoFloorplanScale,
staticValue?.getUnitsPerPixel ?? readDefaultFloorplanScale,
staticValue?.getUnitsPerPixel ?? readDefaultFloorplanScale,
)
}

export function useFloorplanSceneRotation(): number {
return useContext(FloorplanSceneRotationContext)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ import {
collectFloorplanDependencyNodes,
collectFloorplanLinkedLevelNodes,
computeAffectedSiblingIds,
floorplanAffordanceReshapeScope,
floorplanHandleDoubleClickAffordance,
InteractiveGeometry,
splitFloorplanOverlay,
subscribeFloorplanAffordanceToolCancel,
} from './floorplan-registry-layer'

describe('floorplan affordance ownership', () => {
test('keeps the wall center curve drag owned by the floorplan dispatcher', () => {
expect(floorplanAffordanceReshapeScope('wall-curve', 'wall_1', undefined)).toEqual({
kind: 'reshaping',
nodeId: 'wall_1',
reshape: 'curve',
driver: 'floorplan',
})
})
})

function cabinetRun(id: string, children: string[] = [], parentId: string | null = 'level_test') {
return {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import {
type ComponentProps,
memo,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
Expand Down Expand Up @@ -88,7 +89,11 @@ import {
startFloorplanGroupMove,
startFloorplanGroupRotate,
} from '../floorplan-group-move'
import { useFloorplanSceneRotation, useFloorplanStaticRender } from '../floorplan-render-context'
import {
useFloorplanSceneRotation,
useFloorplanStaticRender,
useFloorplanStaticUnitsPerPixel,
} from '../floorplan-render-context'
import {
floorplanAnnotationObstacleMode,
isFloorplanAnnotationObstacleGeometry,
Expand Down Expand Up @@ -142,6 +147,13 @@ const DIRECT_DRAG_THRESHOLD_PX = 4
const DIRECT_ROTATE_EPSILON = 1e-6
const DIRECT_ROTATE_RADIANS_PER_PIXEL = Math.PI / 180

const ScaleAwareFloorplanGroupSelectionBox = memo(function ScaleAwareFloorplanGroupSelectionBox(
props: Omit<ComponentProps<typeof FloorplanGroupSelectionBox>, 'unitsPerPixel'>,
) {
const unitsPerPixel = useFloorplanStaticUnitsPerPixel()
return <FloorplanGroupSelectionBox {...props} unitsPerPixel={unitsPerPixel} />
})

/**
* Snapshot of node fields captured at drag-start, used by the single-undo
* dance to revert untracked before re-applying as a single tracked
Expand Down Expand Up @@ -235,38 +247,38 @@ export function subscribeFloorplanAffordanceToolCancel(
// affordances return `null` (no polygon/wall snapping chip). Keyed off the
// affordance name the kinds register (`move-vertex` / `move-edge` / `add-vertex`
// / `curve` / `move-endpoint`).
function affordanceReshapeScope(
export function floorplanAffordanceReshapeScope(
affordance: string,
nodeId: string,
payload: unknown,
): ActiveInteractionScope | null {
if (affordance.includes('vertex') || affordance.includes('edge')) {
const holeIndex = (payload as { holeIndex?: number } | undefined)?.holeIndex
return holeIndex !== undefined
? holeEditScope({ nodeId, holeIndex })
: boundaryReshapeScope(nodeId)
? holeEditScope({ nodeId, holeIndex, driver: 'floorplan' })
: boundaryReshapeScope(nodeId, 'floorplan')
}
if (affordance.includes('curve')) {
return curveReshapeScope(nodeId)
return curveReshapeScope(nodeId, 'floorplan')
}
if (affordance.includes('control-point')) {
const index = (payload as { index?: number } | undefined)?.index ?? 0
return controlPointReshapeScope(nodeId, index)
return controlPointReshapeScope(nodeId, index, 'floorplan')
}
if (affordance.includes('tangent')) {
const target = payload as { index?: number; side?: 'in' | 'out' } | undefined
return tangentReshapeScope(nodeId, target?.index ?? 0, target?.side ?? 'out')
return tangentReshapeScope(nodeId, target?.index ?? 0, target?.side ?? 'out', 'floorplan')
}
if (affordance.includes('endpoint')) {
const endpoint = (payload as { endpoint?: 'start' | 'end' } | undefined)?.endpoint ?? 'end'
return endpointReshapeScope(nodeId, endpoint)
return endpointReshapeScope(nodeId, endpoint, 'floorplan')
}
// Roof-segment width/depth resize — a no-angle dimension edit, so the
// no-angle 'polygon' snap set (grid / lines / off) via a boundary scope.
// Matched exactly so a still-legacy `*-resize` affordance on another kind
// doesn't get a chip its snap math can't honour yet.
if (affordance === 'roof-segment-resize') {
return boundaryReshapeScope(nodeId)
return boundaryReshapeScope(nodeId, 'floorplan')
}
// 2D corner rotate-arrow (column / elevator / roof-segment / shelf / spawn /
// stair). Begin the same handle-drag scope the 3D rotate gizmo uses, label-
Expand Down Expand Up @@ -1059,7 +1071,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
// the right chip during the edit AND `getActiveSnapContext()` resolves the
// polygon / wall mode-set the affordance's snap math reads. Torn down on
// release / cancel below. `null` for resize / rotate (no snapping chip).
const reshapeScope = affordanceReshapeScope(affordance, nodeId, payload)
const reshapeScope = floorplanAffordanceReshapeScope(affordance, nodeId, payload)
if (reshapeScope) {
useInteractionScope.getState().begin(reshapeScope)
}
Expand Down Expand Up @@ -1305,7 +1317,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
const entries = floorplanData.entries
if (entries.length === 0) return null

const unitsPerPixel = renderCtx?.getUnitsPerPixel() ?? 1
const palette = renderCtx?.palette

return (
Expand Down Expand Up @@ -1374,7 +1385,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
unit={unit}
metricNotation={metricNotation}
wallDimensionReference={wallDimensionReference}
unitsPerPixel={unitsPerPixel}
visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
ctxOverrides={entry.ctxOverrides}
/>
Expand Down Expand Up @@ -1428,7 +1438,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
unit={unit}
metricNotation={metricNotation}
wallDimensionReference={wallDimensionReference}
unitsPerPixel={unitsPerPixel}
visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
ctxOverrides={entry.ctxOverrides}
/>
Expand All @@ -1438,11 +1447,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
{/* Dashed group bbox — shows what a group drag carries along while a
multi-selection exists, rides the live delta mid-drag, and doubles
as the group's whole-area drag handle. */}
<FloorplanGroupSelectionBox
<ScaleAwareFloorplanGroupSelectionBox
onPointerDown={handleGroupBoxPointerDown}
onRotatePointerDown={handleGroupBoxRotatePointerDown}
palette={palette}
unitsPerPixel={unitsPerPixel}
/>
{/* Transient live-rotation readout — drawn last so the wedge + degree
chip sit above all handle chrome while a rotate-arrow is dragged. */}
Expand All @@ -1451,7 +1459,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
overlay={rotationOverlay}
palette={palette}
sceneRotationDeg={sceneRotationDeg}
unitsPerPixel={unitsPerPixel}
/>
) : null}
</g>
Expand Down Expand Up @@ -1775,7 +1782,6 @@ type FloorplanRegistryEntryProps = {
unit: 'metric' | 'imperial'
metricNotation: 'meters' | 'millimeters'
wallDimensionReference: FloorplanWallDimensionReference
unitsPerPixel: number
visibilityRootId: AnyNodeId | undefined
}

Expand Down Expand Up @@ -1818,7 +1824,6 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
unit,
metricNotation,
wallDimensionReference,
unitsPerPixel,
visibilityRootId,
}: FloorplanRegistryEntryProps): React.ReactElement | null {
const live = useLiveTransforms((s) => (floorplanVisible ? s.transforms.get(nodeId) : undefined))
Expand Down Expand Up @@ -1969,7 +1974,6 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
onMoveHandlePointerDown={handleMoveHandlePointerDown}
palette={palette}
sceneRotationDeg={sceneRotationDeg}
unitsPerPixel={unitsPerPixel}
/>
</g>
)
Expand Down Expand Up @@ -2242,7 +2246,7 @@ export function getFloorplanLevelData(

type InteractiveGeometryProps = {
geometry: FloorplanGeometry
unitsPerPixel: number
unitsPerPixel?: number
palette: FloorplanPalette | undefined
hatchPatternId: string | undefined
hoveredHandleId: string | null
Expand Down Expand Up @@ -2270,7 +2274,7 @@ type InteractiveGeometryProps = {

export const InteractiveGeometry = memo(function InteractiveGeometry({
geometry,
unitsPerPixel,
unitsPerPixel: unitsPerPixelOverride,
palette,
hatchPatternId,
hoveredHandleId,
Expand All @@ -2284,6 +2288,9 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
onHandlePointerDown,
onMoveHandlePointerDown,
}: InteractiveGeometryProps): React.ReactElement {
const liveUnitsPerPixel = useFloorplanStaticUnitsPerPixel()
const unitsPerPixel = unitsPerPixelOverride ?? liveUnitsPerPixel

return renderInteractive(geometry, 0)

function renderInteractive(g: FloorplanGeometry, keyHint: number): React.ReactElement {
Expand Down Expand Up @@ -3491,17 +3498,19 @@ const ROTATION_WEDGE_SEGMENTS = 48
export function RotationAngleOverlay({
overlay,
palette,
unitsPerPixel,
unitsPerPixel: unitsPerPixelOverride,
sceneRotationDeg,
}: {
overlay: RotationOverlayState
palette: Pick<
FloorplanPalette,
'measurementLabelBackground' | 'measurementLabelText' | 'measurementStroke'
>
unitsPerPixel: number
unitsPerPixel?: number
sceneRotationDeg: number
}): React.ReactElement {
const liveUnitsPerPixel = useFloorplanStaticUnitsPerPixel()
const unitsPerPixel = unitsPerPixelOverride ?? liveUnitsPerPixel
const { pivot, startAngle, endAngle, radius, sweep } = overlay
const span = endAngle - startAngle
const count = Math.max(8, Math.ceil((Math.abs(span) / Math.PI) * ROTATION_WEDGE_SEGMENTS))
Expand Down
10 changes: 8 additions & 2 deletions packages/editor/src/components/editor/custom-camera-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
type CameraPoseApplicationPlan,
normalizeCameraPose,
planCameraPoseApplication,
publishInitialCameraPose,
releaseCameraPoseEventSuppression,
stepCameraPoseInterpolation,
withCameraPoseDistance,
} from '../../lib/camera-pose'
Expand Down Expand Up @@ -609,6 +611,10 @@ export const CustomCameraControls = () => {
publishCurrentPose()
}, [publishCurrentPose])

useEffect(() => {
publishInitialCameraPose(publishCurrentPose)
}, [publishCurrentPose])

useFrame((_, delta) => {
if (isFirstPersonMode || !controls.current) return

Expand Down Expand Up @@ -645,12 +651,12 @@ export const CustomCameraControls = () => {
} catch {
if (activePoseInterpolation.current === activePose) {
activePoseInterpolation.current = null
suppressPoseEvents.current = false
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
}
}
if (step.settled && activePoseInterpolation.current === activePose) {
activePoseInterpolation.current = null
suppressPoseEvents.current = false
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
}
}
}
Expand Down
Loading
Loading