Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
650abd6
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 27, 2026
1f8dc8b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 28, 2026
7c4b791
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 31, 2026
ef7184f
Merge branch 'main' of github.com:sudhir9297/editor; branch 'main' of…
sudhir9297 Aug 10, 2026
c92a376
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Aug 12, 2026
da49781
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Aug 19, 2026
228b233
feat(nodes): improve block edit interactions
sudhir9297 Aug 20, 2026
528ec46
fix(editor): detach items leaving block faces
sudhir9297 Aug 20, 2026
9b99730
feat(block): improve edit mode interactions
sudhir9297 Aug 20, 2026
39f9e2e
feat(block): support multi-component mesh operations
sudhir9297 Aug 20, 2026
bc87ca2
feat(block): assign accent material to new slots
sudhir9297 Aug 20, 2026
df066c8
refactor(block): isolate edit mode services
sudhir9297 Aug 20, 2026
6c7452d
fix(block): preserve constrained transform intent
sudhir9297 Aug 20, 2026
b15178b
fix(block): key last operation controls
sudhir9297 Aug 20, 2026
384eb92
docs: remove stale wiki research notes
sudhir9297 Aug 20, 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
@@ -1,9 +1,18 @@
'use client'

import { type AnyNodeId, useScene } from '@pascal-app/core'
import {
type AnyNodeId,
createSceneApi,
runAsSingleSceneHistoryStep,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { type ComponentType, Suspense, useMemo } from 'react'
import { getRegistryAffordanceTool } from '../tools/shared/affordance-dispatch'
import type {
SelectionAffordanceHistoryApi,
SelectionAffordanceProps,
} from './selection-affordance-services'

/**
* Editor-mounted dispatcher for a kind's selection-time editing UI.
Expand All @@ -19,20 +28,43 @@ import { getRegistryAffordanceTool } from '../tools/shared/affordance-dispatch'
*/
export function SelectionAffordanceManager() {
const selectedIds = useViewer((s) => s.selection.selectedIds)
const selectedKind = useScene((s) => {
const selectedNode = useScene((s) => {
if (selectedIds.length !== 1) return null
return s.nodes[selectedIds[0] as AnyNodeId]?.type ?? null
return s.nodes[selectedIds[0] as AnyNodeId] ?? null
})
const readOnly = useScene((s) => s.readOnly)
const sceneApi = useMemo(() => createSceneApi(useScene), [])
const historyApi = useMemo<SelectionAffordanceHistoryApi>(
() => ({
depth: () => useScene.temporal.getState().pastStates.length,
replaceLatest: (expectedDepth, replace) => {
if (useScene.temporal.getState().pastStates.length !== expectedDepth) return false
let replaced = false
runAsSingleSceneHistoryStep(useScene, () => {
useScene.temporal.getState().undo()
replaced = replace()
if (!replaced) useScene.temporal.getState().redo()
})
return replaced
},
}),
[],
)

const Component = useMemo<ComponentType | null>(() => {
if (!selectedKind) return null
return getRegistryAffordanceTool(selectedKind, 'selection')
}, [selectedKind])
const Component = useMemo<ComponentType<SelectionAffordanceProps> | null>(() => {
if (!selectedNode) return null
return getRegistryAffordanceTool(selectedNode.type, 'selection')
}, [selectedNode])

if (!Component) return null
if (!(Component && selectedNode)) return null
return (
<Suspense fallback={null}>
<Component />
<Component
historyApi={historyApi}
node={selectedNode}
readOnly={readOnly}
sceneApi={sceneApi}
/>
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AnyNode, SceneApi } from '@pascal-app/core'

export type SelectionAffordanceHistoryApi = {
depth: () => number
replaceLatest: (expectedDepth: number, replace: () => boolean) => boolean
}

export type SelectionAffordanceProps = {
historyApi: SelectionAffordanceHistoryApi
node: AnyNode
readOnly: boolean
sceneApi: SceneApi
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ describe('resolveBlockFaceSwitch', () => {
})

describe('shouldDetachBlockFaceOnLeave', () => {
test('keeps attached items on the last valid block face during transient leave events', () => {
expect(shouldDetachBlockFaceOnLeave('wall')).toBe(false)
expect(shouldDetachBlockFaceOnLeave('wall-side')).toBe(false)
expect(shouldDetachBlockFaceOnLeave('ceiling')).toBe(false)
test('detaches attached items when they leave a block face', () => {
expect(shouldDetachBlockFaceOnLeave('wall')).toBe(true)
expect(shouldDetachBlockFaceOnLeave('wall-side')).toBe(true)
expect(shouldDetachBlockFaceOnLeave('ceiling')).toBe(true)
})

test('allows free floor items to leave a block face', () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/editor/src/components/tools/item/block-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export function resolveBlockFaceSwitch(
}

export function shouldDetachBlockFaceOnLeave(attachTo: string | undefined): boolean {
return !attachTo
return (
attachTo === undefined ||
attachTo === 'wall' ||
attachTo === 'wall-side' ||
attachTo === 'ceiling'
)
}

export function clampBlockFacePosition(
Expand Down
20 changes: 19 additions & 1 deletion packages/editor/src/hooks/use-keyboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from '@pascal-app/core'
import { meshEditScope } from '../lib/interaction/scope'
import useInteractionScope from '../store/use-interaction-scope'
import { runHistoryShortcut } from './use-keyboard'
import {
canCycleSnappingModeShortcut,
canRunGlobalRotationShortcut,
runHistoryShortcut,
} from './use-keyboard'

type RafFn = (callback: (time: number) => void) => number
;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (
Expand Down Expand Up @@ -42,6 +46,20 @@ afterEach(() => {
})

describe('history shortcuts during block editing', () => {
test('reserves R for the active mesh editor', () => {
expect(canRunGlobalRotationShortcut()).toBe(true)
useInteractionScope.getState().begin(meshEditScope(NODE_ID))
expect(canRunGlobalRotationShortcut()).toBe(false)
})

test('reserves held Shift for precision while a mesh operation is active', () => {
useInteractionScope.getState().begin(meshEditScope(NODE_ID))
expect(canCycleSnappingModeShortcut(true)).toBe(true)

useInteractionScope.getState().begin(meshEditScope(NODE_ID, 'operating', 'translate'))
expect(canCycleSnappingModeShortcut(true)).toBe(false)
})

test('undoes and redoes mesh changes without leaving component selection mode', () => {
useInteractionScope.getState().begin(meshEditScope(NODE_ID))

Expand Down
16 changes: 12 additions & 4 deletions packages/editor/src/hooks/use-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ export const runHistoryShortcut = (direction: 'undo' | 'redo') => {
return true
}

export const canRunGlobalRotationShortcut = () =>
useInteractionScope.getState().scope.kind !== 'mesh-editing'

export const canCycleSnappingModeShortcut = (hasActiveContext = getActiveSnapContext() != null) => {
const scope = useInteractionScope.getState().scope
return hasActiveContext && !(scope.kind === 'mesh-editing' && scope.phase === 'operating')
}

export const useKeyboard = ({
isVersionPreviewMode = false,
disabled = false,
Expand Down Expand Up @@ -200,7 +208,6 @@ export const useKeyboard = ({
// every node move (including wall-hosted items + door/window openings, which
// now declare `snapProfile`), and endpoint/polygon reshaping, so the keys
// never silently stop working. Force-place lives on Alt where a tool supports it.
const isSnappingCycleContext = () => getActiveSnapContext() != null
// A "clean tap" of Ctrl/Meta (pressed and released with NO other key in
// between) cycles the grid step — same context as the Shift snapping-mode
// cycle. `ctrlTapClean` starts true the moment Ctrl/Meta goes down alone
Expand Down Expand Up @@ -270,7 +277,7 @@ export const useKeyboard = ({
return
}

if (e.key === 'Shift' && !e.repeat && isSnappingCycleContext()) {
if (e.key === 'Shift' && !e.repeat && canCycleSnappingModeShortcut()) {
// Cycle the global snapping mode (grid → lines → angles → off).
// `'off'` is the snap bypass now, so Shift no longer holds-to-bypass.
e.preventDefault()
Expand Down Expand Up @@ -480,7 +487,8 @@ export const useKeyboard = ({
!e.metaKey &&
!e.ctrlKey &&
!isVersionPreviewMode &&
!isPlacingOpening()
!isPlacingOpening() &&
canRunGlobalRotationShortcut()
) {
// `!metaKey && !ctrlKey` lets Cmd/Ctrl+R reach the browser reload instead
// of rotating/flipping the selected node.
Expand Down Expand Up @@ -699,7 +707,7 @@ export const useKeyboard = ({
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return
}
if (!isSnappingCycleContext()) return
if (!canCycleSnappingModeShortcut()) return
// Cycle the grid / measurement step (0.5 → 0.25 → 0.1 → 0.05).
useEditor.getState().cycleGridSnapStep()
sfxEmitter.emit('sfx:grid-snap')
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export {
buildSvgArrowHeadPoints,
getArcPlanPoint,
} from './components/editor-2d/svg-paths'
export type {
SelectionAffordanceHistoryApi,
SelectionAffordanceProps,
} from './components/systems/selection-affordance-services'
// Phase 5 Stage D transitional exports — pure drafting / angle helpers
// consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
// cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
Expand Down
Loading
Loading