Anton f06f2fa1b1
feat: 2D editing — floorplan panel, measurements, command palette (v0.3.0)
* sync: port 2D editing features from monorepo (v0.3.0)

## New Features

### Floorplan Panel (7.5K LOC)
- Full 2D editing interface with wall drawing, measurement, and unit display
- Interactive floorplan view with pan/zoom, grid snapping
- Wall thickness visualization, door/window placement
- Zone and slab polygon editing
- Guide image overlay support
- Metric/imperial unit toggle

### Command Palette Overhaul
- Complete rewrite with new command registry system
- Keyboard shortcuts UI with ShortcutToken component
- Editor-specific commands (floorplan, measurements, camera focus)
- Improved search and action organization

### Wall Measurements
- Real-time wall length labels in 3D view
- Metric/imperial conversion
- Wall measurement UI component

### Enhanced Tools
- Wall drafting utilities (grid snapping, validation)
- Node action menu for quick operations
- Improved polygon editing for zones/slabs/sites
- Roof segment panel for granular roof control

## Package Changes

### @pascal-app/core@0.3.0
- New wall-footprint.ts: 2D wall footprint calculation
- Wall mitering exports for floorplan view
- camera-controls:focus event
- Space detection undo pause/resume
- Mark sibling nodes dirty on deletion (miter recalc)

### @pascal-app/viewer@0.3.0
- ErrorBoundary component for robust item rendering
- Broken item fallback UI
- Wall renderer: mark dirty on mount
- Ground occluder: only lowest level punches through
- Unit state (metric/imperial) in viewer store

### @pascal-app/editor@0.1.0
- 48 file changes (16 new, 32 modified)
- New stores: useCommandRegistry, usePaletteViewRegistry
- Tree node drag-and-drop system
- Level selection utilities
- Enhanced scene graph operations

## Apps/Editor
- GeistPixelSquare font for pixel-perfect UI
- Blueprint icon asset
- Updated layout and globals for font support

## Security
- Zero AI imports or internal package refs
- All monorepo-specific code excluded
- Clean audit: no API keys or secrets

## Testing
- Security audit passed ✅
- All AI/internal code excluded ✅
- Version bumps applied ✅

79 files modified, 16 files added
~2600 insertions, ~1400 deletions

* fix: lint cleanup — suppress intentional dep warnings, fix missing dep, remove stale ignores

- Add biome-ignore for 3 intentional useEffect reset patterns (levelId, selectedGuide, selectedId)
- Fix actual missing dependency: currentBuildingId in handleSiteEditShortcutSelect callback
- Remove 3 stale biome-ignore comments in r3f.d.ts (rule not active)

Build and lint pass clean.

* fix: type errors — polygon area guards, selection cast, readonly keywords, door guard

- ceiling-panel, slab-panel, ceiling-tree-node, slab-tree-node, zone-tree-node:
  guard polygon[i]/polygon[j] array access before arithmetic (TS2532)
- scene.ts: introduce toViewerSelection() helper to cast persisted string IDs
  to branded template literal types expected by useViewer.setSelection (TS2345)
- door-panel: guard early return when node is undefined in setSegmentHeightRatio (TS18048)
- editor-commands: remove 'as const' from inline command object, keywords is mutable string[] (TS2322)

All type checks pass. Build and lint clean.

---------

Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com>
2026-03-24 19:48:11 +00:00

114 lines
2.9 KiB
TypeScript

import type { ThreeEvent } from '@react-three/fiber'
import mitt from 'mitt'
import type {
BuildingNode,
CeilingNode,
DoorNode,
ItemNode,
LevelNode,
RoofNode,
RoofSegmentNode,
SiteNode,
SlabNode,
WallNode,
WindowNode,
ZoneNode,
} from '../schema'
import type { AnyNode } from '../schema/types'
// Base event interfaces
export interface GridEvent {
position: [number, number, number]
nativeEvent: ThreeEvent<PointerEvent>
}
export interface NodeEvent<T extends AnyNode = AnyNode> {
node: T
position: [number, number, number]
localPosition: [number, number, number]
normal?: [number, number, number]
stopPropagation: () => void
nativeEvent: ThreeEvent<PointerEvent>
}
export type WallEvent = NodeEvent<WallNode>
export type ItemEvent = NodeEvent<ItemNode>
export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type LevelEvent = NodeEvent<LevelNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type SlabEvent = NodeEvent<SlabNode>
export type CeilingEvent = NodeEvent<CeilingNode>
export type RoofEvent = NodeEvent<RoofNode>
export type RoofSegmentEvent = NodeEvent<RoofSegmentNode>
export type WindowEvent = NodeEvent<WindowNode>
export type DoorEvent = NodeEvent<DoorNode>
// Event suffixes - exported for use in hooks
export const eventSuffixes = [
'click',
'move',
'enter',
'leave',
'pointerdown',
'pointerup',
'context-menu',
'double-click',
] as const
export type EventSuffix = (typeof eventSuffixes)[number]
type NodeEvents<T extends string, E> = {
[K in `${T}:${EventSuffix}`]: E
}
type GridEvents = {
[K in `grid:${EventSuffix}`]: GridEvent
}
export interface CameraControlEvent {
nodeId: AnyNode['id']
}
export interface ThumbnailGenerateEvent {
projectId: string
}
type CameraControlEvents = {
'camera-controls:view': CameraControlEvent
'camera-controls:focus': CameraControlEvent
'camera-controls:capture': CameraControlEvent
'camera-controls:top-view': undefined
'camera-controls:orbit-cw': undefined
'camera-controls:orbit-ccw': undefined
'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
}
type ToolEvents = {
'tool:cancel': undefined
}
type PresetEvents = {
'preset:generate-thumbnail': { presetId: string; nodeId: string }
'preset:thumbnail-updated': { presetId: string; thumbnailUrl: string }
}
type EditorEvents = GridEvents &
NodeEvents<'wall', WallEvent> &
NodeEvents<'item', ItemEvent> &
NodeEvents<'site', SiteEvent> &
NodeEvents<'building', BuildingEvent> &
NodeEvents<'level', LevelEvent> &
NodeEvents<'zone', ZoneEvent> &
NodeEvents<'slab', SlabEvent> &
NodeEvents<'ceiling', CeilingEvent> &
NodeEvents<'roof', RoofEvent> &
NodeEvents<'roof-segment', RoofSegmentEvent> &
NodeEvents<'window', WindowEvent> &
NodeEvents<'door', DoorEvent> &
CameraControlEvents &
ToolEvents &
PresetEvents
export const emitter = mitt<EditorEvents>()