Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
5d2d446
perf(editor): reduce floorplan annotation navigation work
sudhir9297 Jul 24, 2026
d7d1252
perf(editor): reduce floorplan camera update work
sudhir9297 Jul 24, 2026
999baaa
perf(editor): isolate floorplan pose updates
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
1 change: 0 additions & 1 deletion packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ type CameraControlEvents = {
'camera-controls:orbit-ccw': undefined
'camera-controls:fit-scene': CameraControlFitSceneEvent
'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
'camera-controls:pose': CameraPose
'camera-controls:apply-pose': CameraPose
'camera-controls:cancel-pose': undefined
'camera-controls:interaction-start': undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, test } from 'bun:test'
import { createFloorplanRenderScaleReference } from './floorplan-render-context'

describe('floorplan render context', () => {
test('updates the live scale without changing the registry-facing reader', () => {
const scale = createFloorplanRenderScaleReference(0.02)
const read = scale.read

scale.update(0.01)

expect(scale.read).toBe(read)
expect(read()).toBe(0.01)
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

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

/**
* Per-frame render context shared between the legacy `floorplan-panel.tsx`
Expand Down Expand Up @@ -34,20 +34,65 @@ export type FloorplanRenderContextValue = {
sceneRotationDeg: number
}

const FloorplanRenderContext = createContext<FloorplanRenderContextValue | null>(null)
export type FloorplanStaticRenderContextValue = Omit<
FloorplanRenderContextValue,
'sceneRotationDeg' | 'unitsPerPixel'
> & {
getSceneRotationDeg: () => number
getUnitsPerPixel: () => number
}

const FloorplanStaticRenderContext = createContext<FloorplanStaticRenderContextValue | null>(null)
const FloorplanSceneRotationContext = createContext(0)
const FloorplanUnitsPerPixelContext = createContext(1)

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

export function createFloorplanRenderScaleReference(
initialUnitsPerPixel: number,
): FloorplanRenderScaleReference {
let unitsPerPixel = initialUnitsPerPixel
return {
read: () => unitsPerPixel,
update: (nextUnitsPerPixel) => {
unitsPerPixel = nextUnitsPerPixel
},
}
}

export function FloorplanRenderProvider({
children,
unitsPerPixel,
palette,
hatchPatternId,
sceneRotationDeg,
}: FloorplanRenderContextValue & { children: ReactNode }) {
const value = useMemo<FloorplanRenderContextValue>(
() => ({ unitsPerPixel, palette, hatchPatternId, sceneRotationDeg }),
[unitsPerPixel, palette, hatchPatternId, sceneRotationDeg],
getSceneRotationDeg,
}: FloorplanRenderContextValue & {
children: ReactNode
getSceneRotationDeg: () => number
}) {
const renderScaleReference = useRef<FloorplanRenderScaleReference | null>(null)
if (!renderScaleReference.current) {
renderScaleReference.current = createFloorplanRenderScaleReference(unitsPerPixel)
}
renderScaleReference.current.update(unitsPerPixel)
const getUnitsPerPixel = renderScaleReference.current.read
const staticValue = useMemo<FloorplanStaticRenderContextValue>(
() => ({ palette, hatchPatternId, getSceneRotationDeg, getUnitsPerPixel }),
[palette, hatchPatternId, getSceneRotationDeg, getUnitsPerPixel],
)
return (
<FloorplanStaticRenderContext.Provider value={staticValue}>
<FloorplanUnitsPerPixelContext.Provider value={unitsPerPixel}>
<FloorplanSceneRotationContext.Provider value={sceneRotationDeg}>
{children}
</FloorplanSceneRotationContext.Provider>
</FloorplanUnitsPerPixelContext.Provider>
</FloorplanStaticRenderContext.Provider>
)
return <FloorplanRenderContext.Provider value={value}>{children}</FloorplanRenderContext.Provider>
}

/**
Expand All @@ -58,5 +103,27 @@ export function FloorplanRenderProvider({
* whole legacy panel along.
*/
export function useFloorplanRender(): FloorplanRenderContextValue | null {
return useContext(FloorplanRenderContext)
const staticValue = useContext(FloorplanStaticRenderContext)
const sceneRotationDeg = useContext(FloorplanSceneRotationContext)
const unitsPerPixel = useContext(FloorplanUnitsPerPixelContext)
return useMemo(
() =>
staticValue
? {
unitsPerPixel,
palette: staticValue.palette,
hatchPatternId: staticValue.hatchPatternId,
sceneRotationDeg,
}
: null,
[sceneRotationDeg, staticValue, unitsPerPixel],
)
}

export function useFloorplanStaticRender(): FloorplanStaticRenderContextValue | null {
return useContext(FloorplanStaticRenderContext)
}

export function useFloorplanSceneRotation(): number {
return useContext(FloorplanSceneRotationContext)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { floorplanGeometryMetadata } from '../../../lib/floorplan/floorplan-exte
import {
collectAnnotationLayoutPreflightIssues,
floorplanAnnotationObstacleMode,
observeSvgAnnotationLayoutChanges,
polylineObstacleRectangles,
resolveAnnotationLabelRectangles,
resolveSvgAnnotationCollisions,
} from './floorplan-annotation-layout'

describe('floorplanAnnotationObstacleMode', () => {
Expand Down Expand Up @@ -333,135 +333,14 @@ describe('resolveAnnotationLabelRectangles', () => {
})
})

describe('observeSvgAnnotationLayoutChanges', () => {
test('requests a fresh collision pass when floor-plan geometry changes after mount', () => {
const OriginalMutationObserver = globalThis.MutationObserver
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
let notify: MutationCallback | undefined
let animationFrames: FrameRequestCallback[] = []
let disconnected = false
let observedOptions: MutationObserverInit | undefined

class FakeMutationObserver {
constructor(callback: MutationCallback) {
notify = callback
}

observe(_target: Node, options?: MutationObserverInit): void {
observedOptions = options
}

disconnect(): void {
disconnected = true
}

takeRecords(): MutationRecord[] {
return []
}
}

globalThis.MutationObserver = FakeMutationObserver as typeof MutationObserver
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
animationFrames.push(callback)
return animationFrames.length
}) as typeof requestAnimationFrame
globalThis.cancelAnimationFrame = (() => {}) as typeof cancelAnimationFrame
try {
const flushAnimationFrame = () => {
const callbacks = animationFrames
animationFrames = []
for (const callback of callbacks) callback(0)
}
let layoutPasses = 0
const stop = observeSvgAnnotationLayoutChanges({} as SVGSVGElement, () => {
layoutPasses += 1
})

notify?.([{ type: 'childList' } as MutationRecord], {} as MutationObserver)

expect(layoutPasses).toBe(0)
flushAnimationFrame()
expect(layoutPasses).toBe(0)
flushAnimationFrame()
expect(layoutPasses).toBe(1)
expect(observedOptions).toMatchObject({
attributes: true,
childList: true,
subtree: true,
attributeFilter: expect.any(Array),
})

notify?.(
[
{
attributeName: 'style',
target: { closest: () => ({}) },
type: 'attributes',
} as unknown as MutationRecord,
],
{} as MutationObserver,
)
expect(layoutPasses).toBe(1)

stop()
expect(disconnected).toBe(true)
} finally {
globalThis.MutationObserver = OriginalMutationObserver
globalThis.requestAnimationFrame = originalRequestAnimationFrame
globalThis.cancelAnimationFrame = originalCancelAnimationFrame
}
})

test('waits for a quiet frame instead of resolving on every mutation frame', () => {
const OriginalMutationObserver = globalThis.MutationObserver
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
let notify: MutationCallback | undefined
let animationFrames: FrameRequestCallback[] = []

class FakeMutationObserver {
constructor(callback: MutationCallback) {
notify = callback
}

observe(): void {}
disconnect(): void {}
takeRecords(): MutationRecord[] {
return []
}
}

globalThis.MutationObserver = FakeMutationObserver as typeof MutationObserver
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
animationFrames.push(callback)
return animationFrames.length
}) as typeof requestAnimationFrame
globalThis.cancelAnimationFrame = (() => {}) as typeof cancelAnimationFrame
try {
const flushAnimationFrame = () => {
const callbacks = animationFrames
animationFrames = []
for (const callback of callbacks) callback(0)
}
let layoutPasses = 0
const stop = observeSvgAnnotationLayoutChanges({} as SVGSVGElement, () => {
layoutPasses += 1
})

for (let frame = 0; frame < 30; frame += 1) {
notify?.([{ type: 'childList' } as MutationRecord], {} as MutationObserver)
flushAnimationFrame()
}
describe('resolveSvgAnnotationCollisions', () => {
test('uses captured label references instead of querying for them again', () => {
const svg = {
querySelectorAll: () => {
throw new Error('labels were rediscovered')
},
} as unknown as SVGSVGElement

expect(layoutPasses).toBe(0)
flushAnimationFrame()
expect(layoutPasses).toBe(1)
stop()
} finally {
globalThis.MutationObserver = OriginalMutationObserver
globalThis.requestAnimationFrame = originalRequestAnimationFrame
globalThis.cancelAnimationFrame = originalCancelAnimationFrame
}
expect(resolveSvgAnnotationCollisions(svg, { labels: [] })).toEqual([])
})
})
Loading
Loading