diff --git a/apps/editor/components/editor/grid.tsx b/apps/editor/components/editor/grid.tsx new file mode 100644 index 00000000..211cdd3d --- /dev/null +++ b/apps/editor/components/editor/grid.tsx @@ -0,0 +1,132 @@ +'use client' + +import { sceneRegistry, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' + +import { useFrame } from '@react-three/fiber' +import { useMemo, useRef, useState } from 'react' +import { MathUtils, type Mesh } from 'three' + +import { color, float, fract, fwidth, mix, positionLocal } from 'three/tsl' +import { MeshBasicNodeMaterial } from 'three/webgpu' +import { useGridEvents } from '@/hooks/use-grid-events' + +export const Grid = ({ + cellSize = 0.5, + cellThickness = 0.5, + cellColor = '#888888', + sectionSize = 1, + sectionThickness = 1, + sectionColor = '#000000', + fadeDistance = 100, + fadeStrength = 1, +}: { + cellSize?: number + cellThickness?: number + cellColor?: string + sectionSize?: number + sectionThickness?: number + sectionColor?: string + fadeDistance?: number + fadeStrength?: number +}) => { + const material = useMemo(() => { + // Use xy since plane geometry is in XY space (before rotation) + const pos = positionLocal.xy + + // Grid line function using fwidth for anti-aliasing + // Returns 1 on grid lines, 0 elsewhere + const getGrid = (size: number, thickness: number) => { + const r = pos.div(size) + const fw = fwidth(r) + // Distance to nearest grid line for each axis + const grid = fract(r.sub(0.5)).sub(0.5).abs() + // Anti-aliased step: divide by fwidth and clamp + const lineX = float(1).sub( + grid.x + .div(fw.x) + .add(1 - thickness) + .min(1), + ) + const lineY = float(1).sub( + grid.y + .div(fw.y) + .add(1 - thickness) + .min(1), + ) + // Combine both axes - max gives us lines in both directions + return lineX.max(lineY) + } + + const g1 = getGrid(cellSize, cellThickness) + const g2 = getGrid(sectionSize, sectionThickness) + + // Distance fade from center + const dist = pos.length() + const fade = float(1).sub(dist.div(fadeDistance).min(1)).pow(fadeStrength) + + // Mix colors based on section grid + const gridColor = mix( + color(cellColor), + color(sectionColor), + float(sectionThickness).mul(g2).min(1), + ) + + // Combined alpha + const alpha = g1.add(g2).mul(fade) + const finalAlpha = mix(alpha.mul(0.75), alpha, g2) + + return new MeshBasicNodeMaterial({ + transparent: true, + colorNode: gridColor, + opacityNode: finalAlpha, + depthWrite: false, + }) + }, [ + cellSize, + cellThickness, + cellColor, + sectionSize, + sectionThickness, + sectionColor, + fadeDistance, + fadeStrength, + ]) + + const gridRef = useRef(null!) + const [gridY, setGridY] = useState(0) + + // Use custom raycasting for grid events (independent of mesh events) + useGridEvents(gridY) + + useFrame((_, delta) => { + const currentLevelId = useViewer.getState().selection.levelId + let targetY = 0 + if (currentLevelId) { + const levelMesh = sceneRegistry.nodes.get(currentLevelId) + if (levelMesh) { + targetY = levelMesh.position.y + } else { + // Fallback: compute from level node data when mesh isn't registered yet + const levelNode = useScene.getState().nodes[currentLevelId] + if (levelNode && 'level' in levelNode) { + const levelMode = useViewer.getState().levelMode + const LEVEL_HEIGHT = 2.5 + const EXPLODED_GAP = 5 + targetY = + ((levelNode as any).level || 0) * + (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0)) + } + } + } + const newY = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta) + gridRef.current.position.y = newY + setGridY(newY) + }) + + return ( + + + + ) +} diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 0d8a8000..4e7da320 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -1,19 +1,7 @@ 'use client' -import { - initSpaceDetectionSync, - initSpatialGridSync, - sceneRegistry, - useScene, -} from '@pascal-app/core' -import { useGridEvents, useViewer, Viewer } from '@pascal-app/viewer' - -import { useFrame } from '@react-three/fiber' -import { useMemo, useRef } from 'react' -import { MathUtils, type Mesh } from 'three' - -import { color, float, fract, fwidth, mix, positionLocal } from 'three/tsl' -import { MeshBasicNodeMaterial } from 'three/webgpu' +import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core' +import { Viewer } from '@pascal-app/viewer' import { useKeyboard } from '@/hooks/use-keyboard' import useEditor from '@/store/use-editor' import { ZoneSystem } from '../systems/zone/zone-system' @@ -24,6 +12,7 @@ import { SidebarProvider } from '../ui/primitives/sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar' import { CustomCameraControls } from './custom-camera-controls' import { ExportManager } from './export-manager' +import { Grid } from './grid' import { SelectionManager } from './selection-manager' useScene.getState().loadScene() @@ -55,116 +44,3 @@ export default function Editor() { ) } - -const Grid = ({ - cellSize = 0.5, - cellThickness = 0.5, - cellColor = '#888888', - sectionSize = 1, - sectionThickness = 1, - sectionColor = '#000000', - fadeDistance = 100, - fadeStrength = 1, -}: { - cellSize?: number - cellThickness?: number - cellColor?: string - sectionSize?: number - sectionThickness?: number - sectionColor?: string - fadeDistance?: number - fadeStrength?: number -}) => { - const material = useMemo(() => { - // Use xy since plane geometry is in XY space (before rotation) - const pos = positionLocal.xy - - // Grid line function using fwidth for anti-aliasing - // Returns 1 on grid lines, 0 elsewhere - const getGrid = (size: number, thickness: number) => { - const r = pos.div(size) - const fw = fwidth(r) - // Distance to nearest grid line for each axis - const grid = fract(r.sub(0.5)).sub(0.5).abs() - // Anti-aliased step: divide by fwidth and clamp - const lineX = float(1).sub( - grid.x - .div(fw.x) - .add(1 - thickness) - .min(1), - ) - const lineY = float(1).sub( - grid.y - .div(fw.y) - .add(1 - thickness) - .min(1), - ) - // Combine both axes - max gives us lines in both directions - return lineX.max(lineY) - } - - const g1 = getGrid(cellSize, cellThickness) - const g2 = getGrid(sectionSize, sectionThickness) - - // Distance fade from center - const dist = pos.length() - const fade = float(1).sub(dist.div(fadeDistance).min(1)).pow(fadeStrength) - - // Mix colors based on section grid - const gridColor = mix( - color(cellColor), - color(sectionColor), - float(sectionThickness).mul(g2).min(1), - ) - - // Combined alpha - const alpha = g1.add(g2).mul(fade) - const finalAlpha = mix(alpha.mul(0.75), alpha, g2) - - return new MeshBasicNodeMaterial({ - transparent: true, - colorNode: gridColor, - opacityNode: finalAlpha, - depthWrite: false, - }) - }, [ - cellSize, - cellThickness, - cellColor, - sectionSize, - sectionThickness, - sectionColor, - fadeDistance, - fadeStrength, - ]) - - const handlers = useGridEvents() - const gridRef = useRef(null!) - - useFrame((_, delta) => { - const currentLevelId = useViewer.getState().selection.levelId - let targetY = 0 - if (currentLevelId) { - const levelMesh = sceneRegistry.nodes.get(currentLevelId) - if (levelMesh) { - targetY = levelMesh.position.y - } else { - // Fallback: compute from level node data when mesh isn't registered yet - const levelNode = useScene.getState().nodes[currentLevelId] - if (levelNode && 'level' in levelNode) { - const levelMode = useViewer.getState().levelMode - const LEVEL_HEIGHT = 2.5 - const EXPLODED_GAP = 5 - targetY = ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0)) - } - } - } - gridRef.current.position.y = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta) - }) - - return ( - - - - ) -} diff --git a/apps/editor/components/tools/ceiling/ceiling-boundary-editor.tsx b/apps/editor/components/tools/ceiling/ceiling-boundary-editor.tsx new file mode 100644 index 00000000..6f8865b7 --- /dev/null +++ b/apps/editor/components/tools/ceiling/ceiling-boundary-editor.tsx @@ -0,0 +1,42 @@ +import { resolveLevelId, type CeilingNode, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useCallback } from 'react' +import { PolygonEditor } from '../shared/polygon-editor' + +interface CeilingBoundaryEditorProps { + ceilingId: CeilingNode['id'] +} + +/** + * Ceiling boundary editor - allows editing ceiling polygon vertices for a specific ceiling + * Uses the generic PolygonEditor component + */ +export const CeilingBoundaryEditor: React.FC = ({ ceilingId }) => { + const ceilingNode = useScene((state) => state.nodes[ceilingId]) + const updateNode = useScene((state) => state.updateNode) + const setSelection = useViewer((state) => state.setSelection) + + const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null + + const handlePolygonChange = useCallback( + (newPolygon: Array<[number, number]>) => { + updateNode(ceilingId, { polygon: newPolygon }) + // Re-assert selection so the ceiling stays selected after the edit + setSelection({ selectedIds: [ceilingId] }) + }, + [ceilingId, updateNode, setSelection], + ) + + if (!ceiling || !ceiling.polygon || ceiling.polygon.length < 3) return null + + return ( + + ) +} diff --git a/apps/editor/components/tools/shared/polygon-editor.tsx b/apps/editor/components/tools/shared/polygon-editor.tsx index 73c06b68..6e57a3e1 100644 --- a/apps/editor/components/tools/shared/polygon-editor.tsx +++ b/apps/editor/components/tools/shared/polygon-editor.tsx @@ -1,15 +1,7 @@ -import { createPortal, useThree } from '@react-three/fiber' -import { sceneRegistry } from '@pascal-app/core' +import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core' +import { createPortal } from '@react-three/fiber' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { - BufferGeometry, - Float32BufferAttribute, - type Mesh, - Plane, - Raycaster, - Vector2, - Vector3, -} from 'three' +import { BufferGeometry, Float32BufferAttribute, type Mesh } from 'three' const Y_OFFSET = 0.02 @@ -45,8 +37,6 @@ export const PolygonEditor: React.FC = ({ levelId, surfaceHeight = 0, }) => { - const { gl, camera } = useThree() - // Get level node from registry if levelId is provided const levelNode = levelId ? sceneRegistry.nodes.get(levelId) : null @@ -59,11 +49,8 @@ export const PolygonEditor: React.FC = ({ const [previewPolygon, setPreviewPolygon] = useState | null>(null) const [hoveredVertex, setHoveredVertex] = useState(null) const [hoveredMidpoint, setHoveredMidpoint] = useState(null) + const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0]) - // Refs for raycasting during drag - const dragPlane = useRef(new Plane(new Vector3(0, 1, 0), -editY)) - dragPlane.current.constant = -editY - const raycaster = useRef(new Raycaster()) const lineRef = useRef(null!) // Track the last polygon prop to detect external changes (undo/redo) @@ -88,30 +75,15 @@ export const PolygonEditor: React.FC = ({ }) }, [displayPolygon]) - // Handle vertex drag + // Update vertex position using grid cursor position const handleVertexDrag = useCallback( - (clientX: number, clientY: number, vertexIndex: number) => { - const canvas = gl.domElement - const rect = canvas.getBoundingClientRect() - const x = ((clientX - rect.left) / rect.width) * 2 - 1 - const y = -((clientY - rect.top) / rect.height) * 2 + 1 - - raycaster.current.setFromCamera(new Vector2(x, y), camera) - const intersection = new Vector3() - raycaster.current.ray.intersectPlane(dragPlane.current, intersection) - - if (intersection) { - // Snap to 0.5 grid - const gridX = Math.round(intersection.x * 2) / 2 - const gridZ = Math.round(intersection.z * 2) / 2 - - const basePolygon = previewPolygon ?? polygon - const newPolygon = [...basePolygon] - newPolygon[vertexIndex] = [gridX, gridZ] - setPreviewPolygon(newPolygon) - } + (vertexIndex: number) => { + const basePolygon = previewPolygon ?? polygon + const newPolygon = [...basePolygon] + newPolygon[vertexIndex] = cursorPosition + setPreviewPolygon(newPolygon) }, - [gl, camera, previewPolygon, polygon], + [cursorPosition, previewPolygon, polygon], ) // Commit polygon changes @@ -152,58 +124,58 @@ export const PolygonEditor: React.FC = ({ [polygon, previewPolygon, onPolygonChange, minVertices], ) - // Set up pointer move/up listeners for dragging with pointer capture + // Listen to grid:move events to track cursor position + useEffect(() => { + const onGridMove = (event: GridEvent) => { + const gridX = Math.round(event.position[0] * 2) / 2 + const gridZ = Math.round(event.position[2] * 2) / 2 + setCursorPosition([gridX, gridZ]) + + // Update vertex position during drag + if (dragState?.isDragging) { + handleVertexDrag(dragState.vertexIndex) + } + } + + emitter.on('grid:move', onGridMove) + return () => { + emitter.off('grid:move', onGridMove) + } + }, [dragState, handleVertexDrag]) + + // Set up pointer up listener for ending drag useEffect(() => { if (!dragState?.isDragging) return - const canvas = gl.domElement - const pointerId = dragState.pointerId - - // Capture pointer to prevent R3F events from firing on other objects (like the grid) - canvas.setPointerCapture(pointerId) - - const handlePointerMove = (e: PointerEvent) => { - handleVertexDrag(e.clientX, e.clientY, dragState.vertexIndex) - } - const handlePointerUp = (e: PointerEvent) => { - // Stop the event from reaching R3F's handlers, which would otherwise - // fire a grid:click and deselect the node being edited. + // Only handle the specific pointer that started the drag + if (e.pointerId !== dragState.pointerId) return + + // Stop the event from propagating to prevent grid click e.stopImmediatePropagation() e.preventDefault() - // Release pointer capture - if (canvas.hasPointerCapture(e.pointerId)) { - canvas.releasePointerCapture(e.pointerId) - } - // Suppress the follow-up click event that browsers fire after pointerup const suppressClick = (ce: MouseEvent) => { ce.stopImmediatePropagation() ce.preventDefault() - canvas.removeEventListener('click', suppressClick, true) + window.removeEventListener('click', suppressClick, true) } - canvas.addEventListener('click', suppressClick, true) + window.addEventListener('click', suppressClick, true) + // Safety cleanup in case no click fires requestAnimationFrame(() => { - canvas.removeEventListener('click', suppressClick, true) + window.removeEventListener('click', suppressClick, true) }) commitPolygonChange() } - canvas.addEventListener('pointermove', handlePointerMove) - canvas.addEventListener('pointerup', handlePointerUp, true) - + window.addEventListener('pointerup', handlePointerUp, true) return () => { - // Release capture on cleanup - if (canvas.hasPointerCapture(pointerId)) { - canvas.releasePointerCapture(pointerId) - } - canvas.removeEventListener('pointermove', handlePointerMove) - canvas.removeEventListener('pointerup', handlePointerUp, true) + window.removeEventListener('pointerup', handlePointerUp, true) } - }, [dragState, gl, handleVertexDrag, commitPolygonChange]) + }, [dragState, commitPolygonChange]) // Update line geometry when polygon changes useEffect(() => { @@ -232,7 +204,7 @@ export const PolygonEditor: React.FC = ({ {/* Border line */} {/* @ts-ignore */} - + {}}> >> = { site: { - "property-line": SiteBoundaryEditor, + 'property-line': SiteBoundaryEditor, }, structure: { wall: WallTool, @@ -27,44 +28,62 @@ const tools: Record>> = { furnish: { item: ItemTool, }, -}; +} export const ToolManager: React.FC = () => { - const phase = useEditor((state) => state.phase); - const mode = useEditor((state) => state.mode); - const tool = useEditor((state) => state.tool); - const movingNode = useEditor((state) => state.movingNode); - const selectedZoneId = useViewer((state) => state.selection.zoneId); - const selectedIds = useViewer((state) => state.selection.selectedIds); - const nodes = useScene((state) => state.nodes); + const phase = useEditor((state) => state.phase) + const mode = useEditor((state) => state.mode) + const tool = useEditor((state) => state.tool) + const movingNode = useEditor((state) => state.movingNode) + const selectedZoneId = useViewer((state) => state.selection.zoneId) + const selectedIds = useViewer((state) => state.selection.selectedIds) + const nodes = useScene((state) => state.nodes) // Check if a slab is selected - const selectedSlabId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === "slab") as SlabNode['id'] | undefined; + const selectedSlabId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === 'slab') as + | SlabNode['id'] + | undefined + + // Check if a ceiling is selected + const selectedCeilingId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === 'ceiling') as + | CeilingNode['id'] + | undefined // Show site boundary editor when in site phase and edit mode - const showSiteBoundaryEditor = phase === "site" && mode === "edit"; + const showSiteBoundaryEditor = phase === 'site' && mode === 'edit' // Show slab boundary editor when in structure/select mode with a slab selected const showSlabBoundaryEditor = - phase === "structure" && mode === "select" && selectedSlabId !== undefined; + phase === 'structure' && mode === 'select' && selectedSlabId !== undefined + + // Show ceiling boundary editor when in structure/select mode with a ceiling selected + const showCeilingBoundaryEditor = + phase === 'structure' && mode === 'select' && selectedCeilingId !== undefined // Show zone boundary editor when in structure/select mode with a zone selected - // Hide when editing a slab to avoid overlapping handles + // Hide when editing a slab or ceiling to avoid overlapping handles const showZoneBoundaryEditor = - phase === "structure" && mode === "select" && selectedZoneId !== null && !showSlabBoundaryEditor; + phase === 'structure' && + mode === 'select' && + selectedZoneId !== null && + !showSlabBoundaryEditor && + !showCeilingBoundaryEditor // Show build tools when in build mode - const showBuildTool = mode === "build" && tool !== null; + const showBuildTool = mode === 'build' && tool !== null - const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null; + const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null return ( <> {showSiteBoundaryEditor && } {showZoneBoundaryEditor && selectedZoneId && } {showSlabBoundaryEditor && selectedSlabId && } + {showCeilingBoundaryEditor && selectedCeilingId && ( + + )} {movingNode && } {!movingNode && BuildToolComponent && } - ); -}; + ) +} diff --git a/apps/editor/hooks/use-grid-events.ts b/apps/editor/hooks/use-grid-events.ts new file mode 100644 index 00000000..2a48514e --- /dev/null +++ b/apps/editor/hooks/use-grid-events.ts @@ -0,0 +1,99 @@ +import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core' +import { useThree } from '@react-three/fiber' +import { useEffect, useRef } from 'react' +import { Plane, Raycaster, Vector2, Vector3 } from 'three' + +/** + * Custom grid events hook that uses manual raycasting instead of mesh events. + * This ensures grid events work even when other meshes block pointer events with stopPropagation. + */ +export function useGridEvents(gridY: number) { + const { camera, gl } = useThree() + const raycaster = useRef(new Raycaster()) + const pointer = useRef(new Vector2()) + const groundPlane = useRef(new Plane(new Vector3(0, 1, 0), 0)) + const intersectionPoint = useRef(new Vector3()) + + // Update ground plane when grid Y changes + useEffect(() => { + groundPlane.current.constant = -gridY + }, [gridY]) + + useEffect(() => { + const canvas = gl.domElement + + const getIntersection = (nativeEvent: MouseEvent | PointerEvent): Vector3 | null => { + // Convert mouse position to normalized device coordinates (-1 to +1) + const rect = canvas.getBoundingClientRect() + pointer.current.x = ((nativeEvent.clientX - rect.left) / rect.width) * 2 - 1 + pointer.current.y = -((nativeEvent.clientY - rect.top) / rect.height) * 2 + 1 + + // Update raycaster + raycaster.current.setFromCamera(pointer.current, camera) + + // Intersect with ground plane + if (raycaster.current.ray.intersectPlane(groundPlane.current, intersectionPoint.current)) { + return intersectionPoint.current.clone() + } + + return null + } + + const emit = (suffix: EventSuffix, nativeEvent: MouseEvent | PointerEvent) => { + const point = getIntersection(nativeEvent) + if (!point) return + + const eventKey = `grid:${suffix}` as `grid:${EventSuffix}` + const payload: GridEvent = { + position: [point.x, point.y, point.z], + nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent + } + + emitter.emit(eventKey, payload) + } + + const handlePointerDown = (e: PointerEvent) => { + if (e.button !== 0) return + emit('pointerdown', e) + } + + const handlePointerUp = (e: PointerEvent) => { + if (e.button !== 0) return + emit('pointerup', e) + } + + const handleClick = (e: PointerEvent) => { + if (e.button !== 0) return + emit('click', e) + } + + const handlePointerMove = (e: PointerEvent) => { + emit('move', e) + } + + const handleDoubleClick = (e: MouseEvent) => { + emit('double-click', e) + } + + const handleContextMenu = (e: MouseEvent) => { + emit('context-menu', e) + } + + // Attach listeners to canvas + canvas.addEventListener('pointerdown', handlePointerDown) + canvas.addEventListener('pointerup', handlePointerUp) + canvas.addEventListener('click', handleClick) + canvas.addEventListener('pointermove', handlePointerMove) + canvas.addEventListener('dblclick', handleDoubleClick) + canvas.addEventListener('contextmenu', handleContextMenu) + + return () => { + canvas.removeEventListener('pointerdown', handlePointerDown) + canvas.removeEventListener('pointerup', handlePointerUp) + canvas.removeEventListener('click', handleClick) + canvas.removeEventListener('pointermove', handlePointerMove) + canvas.removeEventListener('dblclick', handleDoubleClick) + canvas.removeEventListener('contextmenu', handleContextMenu) + } + }, [camera, gl, gridY]) +} diff --git a/packages/viewer/src/hooks/use-grid-events.ts b/packages/viewer/src/hooks/use-grid-events.ts deleted file mode 100644 index 08196a86..00000000 --- a/packages/viewer/src/hooks/use-grid-events.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type EventSuffix, emitter, type GridEvent } from "@pascal-app/core"; -import type { ThreeEvent } from "@react-three/fiber"; - -export function useGridEvents() { - const emit = (suffix: EventSuffix, e: ThreeEvent) => { - const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`; - const payload: GridEvent = { - position: [e.point.x, e.point.y, e.point.z], - nativeEvent: e, - }; - - emitter.emit(eventKey, payload); - }; - - return { - onPointerDown: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("pointerdown", e); - }, - onPointerUp: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("pointerup", e); - }, - onClick: (e: ThreeEvent) => { - if (e.button !== 0) return; - emit("click", e); - }, - onPointerEnter: (e: ThreeEvent) => emit("enter", e), - onPointerLeave: (e: ThreeEvent) => emit("leave", e), - onPointerMove: (e: ThreeEvent) => emit("move", e), - onDoubleClick: (e: ThreeEvent) => emit("double-click", e), - onContextMenu: (e: ThreeEvent) => emit("context-menu", e), - }; -} diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 21aa3ebd..2c417fbe 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -1,4 +1,3 @@ export { default as Viewer } from './components/viewer' -export { useGridEvents } from './hooks/use-grid-events' export { default as useViewer } from './store/use-viewer' export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url' \ No newline at end of file