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
2 changes: 2 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
useLiveTransforms,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { beginPerfAction, cancelPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
import {
type ComponentProps,
memo,
Expand Down Expand Up @@ -247,6 +247,7 @@ export function cancelFloorplanAffordanceDrag(
for (const id of drag.session.affectedIds) effects.clearPreview(id)
effects.endReshapeScope(drag)
effects.clearDragFeedback?.()
cancelPerfAction()
return true
}

Expand Down Expand Up @@ -311,6 +312,14 @@ export function floorplanAffordanceReshapeScope(
return null
}

function floorplanAffordancePerfAction(node: AnyNode, affordance: string): string {
if (affordance.includes('endpoint')) return `drag:${node.type}-endpoint`
if (affordance.includes('resize')) return 'drag:resize'
if (affordance.includes('rotate')) return 'drag:rotate'
if (affordance.includes('move')) return 'drag:move'
return 'drag:reshape'
}

/**
* Transient live-rotation readout state. Rebuilt each pointer-move while a
* rotate-arrow is dragged and cleared on release. World-plan coords.
Expand Down Expand Up @@ -1094,6 +1103,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
event.stopPropagation()
suppressBoxSelectForPointer(event)

beginPerfAction(floorplanAffordancePerfAction(node, affordance), `${node.type}:${node.id}`)
const session = handler.start({
node,
payload,
Expand Down Expand Up @@ -1256,6 +1266,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
drag.historyPaused = false
}
drag.session.commit()
commitPerfAction()
sfxEmitter.emit('sfx:structure-build')
clearSurfacePlanSnapFeedback()
endReshapeScope(drag)
Expand Down Expand Up @@ -1297,6 +1308,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
drag.historyPaused = false
}
useScene.getState().updateNodes(finalUpdates)
commitPerfAction()
sfxEmitter.emit('sfx:structure-build')
} else {
// Either no net change or canCommit() rejected — revert and
Expand All @@ -1310,6 +1322,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
}
const overrides = useLiveNodeOverrides.getState()
for (const id of drag.session.affectedIds) overrides.clear(id)
cancelPerfAction()
}

clearSurfacePlanSnapFeedback()
Expand Down
7 changes: 6 additions & 1 deletion packages/editor/src/components/editor/group-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useLiveTransforms,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import { Plane, Vector2, Vector3 } from 'three'
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
import { clientToPlan } from '../../lib/floorplan/plan-coords'
Expand Down Expand Up @@ -575,6 +575,11 @@ export function deleteSelection(): boolean {
if (selectedIds.length === 0) return false

const commitDelete = () => {
const detail =
selectedIds.length === 1
? (useScene.getState().nodes[selectedIds[0]!]?.type ?? selectedIds[0]!)
: String(selectedIds.length)
markPerfAction('delete', detail)
if (selectedIds.length === 1) {
emitDeleteSFX(useScene.getState().nodes[selectedIds[0]!]?.type)
} else {
Expand Down
26 changes: 24 additions & 2 deletions packages/editor/src/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,22 @@ import {
import {
type HoverStyles,
InteractiveSystem,
PERF_OVERLAY_ENABLED,
recordPerfSample,
SceneEnvironment,
useViewer,
Viewer,
} from '@pascal-app/viewer'
import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import {
memo,
Profiler,
type ProfilerOnRenderCallback,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { ViewerOverlay } from '../../components/viewer-overlay'
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
Expand Down Expand Up @@ -101,6 +112,9 @@ const PAINT_CURSOR_BADGE_OFFSET_X = 14
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
const SCENE_READY_FALLBACK_MS = 8000
type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
}
const EDITOR_HOVER_STYLES: HoverStyles = {
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
Expand Down Expand Up @@ -1196,7 +1210,7 @@ function PreviewStage({
)
}

export default function Editor({
function EditorContent({
layoutVersion = 'v1',
appMenuButton,
sidebarTop,
Expand Down Expand Up @@ -1634,3 +1648,11 @@ export default function Editor({
</div>
)
}

export default function Editor(props: EditorProps) {
return (
<Profiler id="editor" onRender={recordEditorRender}>
<EditorContent {...props} />
</Profiler>
)
}
4 changes: 3 additions & 1 deletion packages/editor/src/components/tools/item/use-draft-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
sceneRegistry,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { beginPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
import { useCallback, useMemo, useRef } from 'react'
import type { Vector3 } from 'three'
import usePlacementPreview from '../../../store/use-placement-preview'
Expand Down Expand Up @@ -220,6 +220,7 @@ export function useDraftNode(): DraftNodeHandle {
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
if (!parentId) return null

beginPerfAction('place:item', draft.id)
// Delete draft while paused (invisible to undo)
useScene.getState().deleteNode(draft.id)
draftRef.current = null
Expand Down Expand Up @@ -268,6 +269,7 @@ export function useDraftNode(): DraftNodeHandle {

adoptedRef.current = false
originalStateRef.current = null
commitPerfAction()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Place action interrupts in-flight gesture

Medium Severity

beginPerfAction('place:item') runs at commit time, not at gesture start. beginPerfAction finalizes any active action as interrupted, so a placing scope opened by setMovingNode (the generic bracket) is cut off at drop. The drag is stored as interrupted and the new receipt only covers the create write.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e51e2a6. Configure here.

return committedNode.id
},
[],
Expand Down
9 changes: 7 additions & 2 deletions packages/editor/src/components/ui/command-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { AnyNodeId, LevelNode } from '@pascal-app/core'
import { useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import { Command, useCommandState } from 'cmdk'
import { ChevronRight, Search } from 'lucide-react'
import type { ReactNode } from 'react'
Expand Down Expand Up @@ -408,7 +408,12 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
key={level.id}
label={getLevelDisplayName(level)}
onSelect={() =>
run(() => useViewer.getState().setSelection({ levelId: level.id }))
run(() => {
if (level.id !== useViewer.getState().selection.levelId) {
markPerfAction('level-switch', level.id)
}
useViewer.getState().setSelection({ levelId: level.id })
})
}
/>
))}
Expand Down
7 changes: 4 additions & 3 deletions packages/editor/src/components/ui/floating-level-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
LevelNode,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import { ClipboardPaste, Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react'
import {
type ButtonHTMLAttributes,
Expand Down Expand Up @@ -631,13 +631,14 @@ export function FloatingLevelSelector() {
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
onPaste={() => handlePasteToLevel(level)}
onRequestDelete={() => setDeletingLevel(level)}
onSelect={() =>
onSelect={() => {
if (!isSelected) markPerfAction('level-switch', level.id)
setSelection(
resolvedBuildingId
? { buildingId: resolvedBuildingId, levelId: level.id }
: { levelId: level.id },
)
}
}}
/>

{showGapBelow && !draggingLevelId && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useScene,
type ZoneNode,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import {
Camera,
ChevronDown,
Expand Down Expand Up @@ -709,7 +709,8 @@ const LevelItem = memo(function LevelItem({
? (level.parentId as BuildingNode['id'])
: undefined

const selectLevel = (levelId: LevelNode['id']) => {
const selectLevel = (levelId: LevelNode['id'], measure = true) => {
if (measure && selectedLevelId !== levelId) markPerfAction('level-switch', levelId)
setSelection(buildingId ? { buildingId, levelId } : { levelId })
}

Expand Down Expand Up @@ -748,7 +749,7 @@ const LevelItem = memo(function LevelItem({
)
}
createNodes(createOps)
selectLevel(newLevelId as LevelNode['id'])
selectLevel(newLevelId as LevelNode['id'], false)
setDuplicateDialogOpen(false)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type LevelNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import { Layers } from 'lucide-react'
import { memo, useCallback, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
Expand Down Expand Up @@ -30,7 +30,10 @@ export const LevelTreeNode = memo(function LevelTreeNode({
const isHovered = useViewer((state) => state.hoveredId === nodeId)
const setSelection = useViewer((state) => state.setSelection)

const handleClick = useCallback(() => setSelection({ levelId: nodeId }), [nodeId, setSelection])
const handleClick = useCallback(() => {
if (!isSelected) markPerfAction('level-switch', nodeId)
setSelection({ levelId: nodeId })
}, [isSelected, nodeId, setSelection])
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
const handleStartEditing = useCallback(() => setIsEditing(true), [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useScene,
type ZoneNode,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
import Link from 'next/link'
import type { ReactNode } from 'react'
Expand Down Expand Up @@ -76,6 +76,7 @@ export const ViewerSceneHeader = ({

const handleLevelClick = (levelId: LevelNode['id']) => {
// When switching levels, deselect zone and items
if (levelId !== selection.levelId) markPerfAction('level-switch', levelId)
useViewer.getState().setSelection({ levelId })
}

Expand Down
5 changes: 4 additions & 1 deletion packages/editor/src/components/viewer/viewer-stage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { markPerfAction, useViewer } from '@pascal-app/viewer'
import type { ReactNode } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
Expand Down Expand Up @@ -134,6 +134,9 @@ export function ViewerStage({
const chooseLevel = useCallback(
(nextLevelId: string, notify = true) => {
setInternalLevelId(nextLevelId)
if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
markPerfAction('level-switch', nextLevelId)
}
selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
if (notify) onLevelChange?.(nextLevelId)
},
Expand Down
20 changes: 17 additions & 3 deletions packages/editor/src/hooks/use-drag-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type SpatialQuery,
useScene,
} from '@pascal-app/core'
import { beginPerfAction, cancelPerfAction, commitPerfAction } from '@pascal-app/viewer'
import { useEffect, useRef } from 'react'

const sceneApi = createSceneApi(useScene)
Expand Down Expand Up @@ -74,14 +75,26 @@ export function useDragAction<Ctx, Draft>(args: UseDragActionArgs<Ctx, Draft>) {
useEffect(() => {
if (!args.active) return

const initial = argsRef.current.initial
const nodeType = initial.node?.type
const isEndpoint = initial.handleId === 'start' || initial.handleId === 'end'
const actionName = isEndpoint && nodeType ? `drag:${nodeType}-endpoint` : 'drag:move'

const session = createDragSession<Ctx, Draft>(argsRef.current.action, sceneApi, {
spatialQuery: argsRef.current.spatialQuery,
childQuery: argsRef.current.childQuery,
onCommit: () => argsRef.current.onCommit?.(),
onCancel: () => argsRef.current.onCancel?.(),
onCommit: () => {
commitPerfAction()
argsRef.current.onCommit?.()
},
onCancel: () => {
cancelPerfAction()
argsRef.current.onCancel?.()
},
})

session.start(argsRef.current.initial)
beginPerfAction(actionName, initial.node?.id ?? nodeType ?? '')
session.start(initial)

const activatedAt = Date.now()
const graceMs = argsRef.current.activationGraceMs ?? 150
Expand Down Expand Up @@ -119,6 +132,7 @@ export function useDragAction<Ctx, Draft>(args: UseDragActionArgs<Ctx, Draft>) {
}
// If the parent flipped `active` to false (or unmounted) while we were
// still mid-drag, treat it as a cancel — no dangling history pause.
if (session.isActive()) cancelPerfAction()
session.dispose()
}
}, [args.active])
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/hooks/use-grid-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type GridEvent,
sceneRegistry,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { timeSpan, useViewer } from '@pascal-app/viewer'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import { Plane, Raycaster, Vector2, Vector3 } from 'three'
Expand Down Expand Up @@ -109,7 +109,7 @@ export function useGridEvents(gridY: number) {

const handlePointerMove = (e: PointerEvent) => {
// Emit move even if camera is dragging, so tools like PolygonEditor still work
emit('move', e)
timeSpan('pointer', () => emit('move', e))
}

const handleDoubleClick = (e: MouseEvent) => {
Expand Down
Loading
Loading