From d36e8fcc7868639700f9c563ac619c9f7aadf2f3 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Wed, 10 Jun 2026 13:56:24 -0400 Subject: [PATCH] fix: zone grid snapping, shelf position panel, deselect on item placement (#394) * fix: zone grid snapping, shelf position panel, deselect on item placement - Zone tool reads the editor's gridSnapStep (0.5/0.25/0.1/0.05) instead of a hardcoded 0.5 for both cursor move and click snapping. - Shelf inspector gains a Position group (vec3 X/Y/Z sliders), matching the item panel. - Item catalog clears the viewer selection before arming placement so shortcuts (rotate & co) don't hit both the ghost and the selected node. Co-Authored-By: Claude Fable 5 * refactor: move level display naming into @pascal-app/core getDefaultLevelName / getLevelDisplayName ("Ground Floor" / "Floor N" / "Basement N") lived in packages/editor's internal lib, so viewer-only surfaces couldn't reach them and fell back to hand-rolled "Level N" labels. The helpers are pure domain logic, so they move to core and export from its barrel; the editor package's seven call sites now import them from there. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- packages/core/src/index.ts | 1 + packages/{editor => core}/src/lib/level-name.ts | 2 +- packages/editor/src/components/tools/zone/zone-tool.tsx | 4 ++-- .../editor/src/components/ui/action-menu/view-toggles.tsx | 2 +- packages/editor/src/components/ui/command-palette/index.tsx | 2 +- .../editor/src/components/ui/floating-level-selector.tsx | 2 +- .../editor/src/components/ui/item-catalog/item-catalog.tsx | 6 +++++- .../editor/src/components/ui/level-duplicate-dialog.tsx | 2 +- .../src/components/ui/sidebar/panels/site-panel/index.tsx | 2 +- .../ui/sidebar/panels/site-panel/level-tree-node.tsx | 2 +- packages/editor/src/components/viewer-overlay.tsx | 2 +- packages/nodes/src/shelf/parametrics.ts | 4 ++++ 12 files changed, 20 insertions(+), 11 deletions(-) rename packages/{editor => core}/src/lib/level-name.ts (86%) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f3acc318..b6ff26f8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -61,6 +61,7 @@ export { isOperationDoorType, SECTIONAL_GARAGE_RENDER_OPEN_SCALE, } from './lib/door-operation' +export { getDefaultLevelName, getLevelDisplayName } from './lib/level-name' export { type Point2D as PolygonPoint2D, pointInPolygon as pointInPolygon2D, diff --git a/packages/editor/src/lib/level-name.ts b/packages/core/src/lib/level-name.ts similarity index 86% rename from packages/editor/src/lib/level-name.ts rename to packages/core/src/lib/level-name.ts index 9476aa5b..dd4f904f 100644 --- a/packages/editor/src/lib/level-name.ts +++ b/packages/core/src/lib/level-name.ts @@ -1,4 +1,4 @@ -import type { LevelNode } from '@pascal-app/core' +import type { LevelNode } from '../schema' export function getDefaultLevelName(level: number): string { if (level === 0) return 'Ground Floor' diff --git a/packages/editor/src/components/tools/zone/zone-tool.tsx b/packages/editor/src/components/tools/zone/zone-tool.tsx index cf43f26c..7ce8f0ae 100644 --- a/packages/editor/src/components/tools/zone/zone-tool.tsx +++ b/packages/editor/src/components/tools/zone/zone-tool.tsx @@ -184,7 +184,7 @@ export const ZoneTool: React.FC = () => { const [gridX, gridZ] = snapWorldXZForActiveBuilding( event.position[0], event.position[2], - 0.5, + useEditor.getState().gridSnapStep, ).local cursorPosition = [gridX, gridZ] levelYRef.current = event.localPosition[1] @@ -217,7 +217,7 @@ export const ZoneTool: React.FC = () => { const [gridX, gridZ] = snapWorldXZForActiveBuilding( event.position[0], event.position[2], - 0.5, + useEditor.getState().gridSnapStep, ).local let clickPoint: [number, number] = [gridX, gridZ] diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index 1e461661..7b5c2a5b 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -13,7 +13,7 @@ import { useViewer } from '@pascal-app/viewer' import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react' import { useCallback, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' -import { getLevelDisplayName } from '../../../lib/level-name' +import { getLevelDisplayName } from '@pascal-app/core' import { createLocalGuideImage } from '../../../lib/local-guide-image' import { cn } from '../../../lib/utils' import useEditor, { type GridSnapStep } from '../../../store/use-editor' diff --git a/packages/editor/src/components/ui/command-palette/index.tsx b/packages/editor/src/components/ui/command-palette/index.tsx index 39b7e012..4f9d2964 100644 --- a/packages/editor/src/components/ui/command-palette/index.tsx +++ b/packages/editor/src/components/ui/command-palette/index.tsx @@ -10,7 +10,7 @@ import { useEffect, useState } from 'react' import { create } from 'zustand' import { useShallow } from 'zustand/shallow' import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog' -import { getLevelDisplayName } from '../../../lib/level-name' +import { getLevelDisplayName } from '@pascal-app/core' import { useCommandRegistry } from '../../../store/use-command-registry' import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry' diff --git a/packages/editor/src/components/ui/floating-level-selector.tsx b/packages/editor/src/components/ui/floating-level-selector.tsx index 1faa7b0d..ef922dfa 100644 --- a/packages/editor/src/components/ui/floating-level-selector.tsx +++ b/packages/editor/src/components/ui/floating-level-selector.tsx @@ -41,7 +41,7 @@ import { buildLevelDuplicateCreateOps, type LevelDuplicatePreset, } from '../../lib/level-duplication' -import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name' +import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core' import { deleteLevelWithFallbackSelection } from '../../lib/level-selection' import { getEditorClipboardSnapshot, diff --git a/packages/editor/src/components/ui/item-catalog/item-catalog.tsx b/packages/editor/src/components/ui/item-catalog/item-catalog.tsx index 8d5206e8..77db922c 100644 --- a/packages/editor/src/components/ui/item-catalog/item-catalog.tsx +++ b/packages/editor/src/components/ui/item-catalog/item-catalog.tsx @@ -1,7 +1,7 @@ 'use client' import type { AssetInput } from '@pascal-app/core' -import { resolveCdnUrl } from '@pascal-app/viewer' +import { resolveCdnUrl, useViewer } from '@pascal-app/viewer' import Image from 'next/image' import { useEffect } from 'react' import { @@ -96,6 +96,10 @@ export function ItemCatalog({ key={index} onClick={() => { triggerSFX('sfx:menu-click') + // Drop the current selection before arming placement — keeping + // it would route shortcuts (rotate & co) to both the ghost and + // the selected node. + useViewer.getState().setSelection({ selectedIds: [], zoneId: null }) setSelectedItem(item) setTool('item') setMode('build') diff --git a/packages/editor/src/components/ui/level-duplicate-dialog.tsx b/packages/editor/src/components/ui/level-duplicate-dialog.tsx index 5b2b9f47..413f60bb 100644 --- a/packages/editor/src/components/ui/level-duplicate-dialog.tsx +++ b/packages/editor/src/components/ui/level-duplicate-dialog.tsx @@ -3,7 +3,7 @@ import type { LevelNode } from '@pascal-app/core' import { useEffect, useState } from 'react' import type { LevelDuplicatePreset } from '../../lib/level-duplication' -import { getLevelDisplayName } from '../../lib/level-name' +import { getLevelDisplayName } from '@pascal-app/core' import { cn } from '../../lib/utils' import { Dialog, diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx index 788054f2..7d02ab61 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx @@ -36,7 +36,7 @@ import { buildLevelDuplicateCreateOps, type LevelDuplicatePreset, } from './../../../../../lib/level-duplication' -import { getDefaultLevelName } from './../../../../../lib/level-name' +import { getDefaultLevelName } from '@pascal-app/core' import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection' import { createLocalGuideImage } from './../../../../../lib/local-guide-image' import { cn } from './../../../../../lib/utils' diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx index 0047dd46..168c39d7 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx @@ -3,7 +3,7 @@ import { useViewer } from '@pascal-app/viewer' import { Layers } from 'lucide-react' import { memo, useCallback, useState } from 'react' import { useShallow } from 'zustand/react/shallow' -import { getDefaultLevelName } from '../../../../../lib/level-name' +import { getDefaultLevelName } from '@pascal-app/core' import { InlineRenameInput } from './inline-rename-input' import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node' import { TreeNodeActions } from './tree-node-actions' diff --git a/packages/editor/src/components/viewer-overlay.tsx b/packages/editor/src/components/viewer-overlay.tsx index 7d60a23b..28f5f876 100644 --- a/packages/editor/src/components/viewer-overlay.tsx +++ b/packages/editor/src/components/viewer-overlay.tsx @@ -6,6 +6,7 @@ import { type AnyNodeId, type BuildingNode, emitter, + getLevelDisplayName, type LevelNode, useScene, type ZoneNode, @@ -30,7 +31,6 @@ import { } from 'lucide-react' import Link from 'next/link' import { useShallow } from 'zustand/react/shallow' -import { getLevelDisplayName } from '../lib/level-name' import { cn } from '../lib/utils' import { ActionButton } from './ui/action-menu/action-button' import { diff --git a/packages/nodes/src/shelf/parametrics.ts b/packages/nodes/src/shelf/parametrics.ts index cf19d743..0f4fdb62 100644 --- a/packages/nodes/src/shelf/parametrics.ts +++ b/packages/nodes/src/shelf/parametrics.ts @@ -76,5 +76,9 @@ export const shelfParametrics: ParametricDescriptor = { { key: 'height', kind: 'number', unit: 'm', min: 0.05, max: 2.5, step: 0.05 }, ], }, + { + label: 'Position', + fields: [{ key: 'position', kind: 'vec3' }], + }, ], }