feat(ui): rebuild the viewer UI with HMDesign components
New React UI around the unchanged engine (#1). GNB_ONLY page: - AppHeader: GNB + GNBBrand; sample drawings in GNBNav (NavigationMenu) and GNBMobileMenu (same data, real ?model= links); file open and about/notice in GNBUtilities. - DwgViewerCanvas: useRef/useEffect wrapper. new Viewer2D(host) on mount, option props diffed and pushed through the engine setters, onSelect/onPointerWorld/onViewChange forwarded to React. Viewer2D has no dispose(), so teardown stops the rAF loop / WebGL context from outside (also needed for StrictMode double mount). - ViewerToolbar: Model/Layout ToggleGroup (sample's visibility rule), Fit, 2.5D and canvas-theme Toggles, window Toggles. - FloatingPanel: HM Card in a fixed frame; drag/collapse via the ported makePanelDraggableAndCollapsible, CSS resize. - LayersPanel (FilterBar search, Switch per layer, show/hide all via LayerPanelManager), PropertiesPanel (formatEntityProperties → Badge + Table, zoom to selection), OptionsPanel (Switch/Slider/Input, IndexedDB through optionsStore, reset). - Load feedback per HMDesign: Progress for the measurable part (download bytes, stage steps) and LoadingScreen for the blocking WASM parse / Viewer2D.load(), painted before each blocking stage. - StatusBar with status line, zoom %, live CAD coordinates in the bottom-right corner; AboutDialog keeps the acadrust MPL-2.0 notice, THIRD-PARTY-NOTICES and the local patch reachable. - Drag & drop, ?model= auto-load, F shortcut, ResizeObserver resync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40befda905
commit
aaa0972bc1
18 files changed
+2163
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from "react"
|
||||
import { ChevronDownIcon, ChevronRightIcon, XIcon } from "lucide-react"
|
||||
import { Button, Card, CardAction, CardHeader, CardTitle, Tooltip, TooltipContent, TooltipTrigger } from "@hm-design/ui"
|
||||
import { makePanelDraggableAndCollapsible } from "../engine/panelUtils"
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
icon: ReactNode
|
||||
/** Extra header content after the title (count badge…). */
|
||||
meta?: ReactNode
|
||||
/** Extra header buttons before collapse / close. */
|
||||
actions?: ReactNode
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
/** Initial slot from the right edge of the anchor (0 = right-most). */
|
||||
slot: number
|
||||
/** Area the window is first placed in (the canvas stage). */
|
||||
anchorRef: RefObject<HTMLElement | null>
|
||||
/** Fixed starting height; omitted = fit content (capped by CSS). */
|
||||
size?: "tall" | "auto"
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const GAP = 12
|
||||
|
||||
/**
|
||||
* A floating tool window over the canvas: HM Card inside a positioned frame.
|
||||
*
|
||||
* Drag (header), collapse (header double-click / button) and focus-to-front come from the ported
|
||||
* `makePanelDraggableAndCollapsible` (src/engine/panelUtils.ts, unchanged); resizing is CSS `resize: both` on the
|
||||
* frame, as in the sample. The frame keeps a static className because panelUtils toggles `collapsed` on it, and
|
||||
* React would overwrite a changing className. Open/closed only touches `style.display`.
|
||||
*/
|
||||
export function FloatingPanel({ title, icon, meta, actions, open, onClose, slot, anchorRef, size = "tall", children }: Props) {
|
||||
const frameRef = useRef<HTMLElement>(null)
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
const bodyRef = useRef<HTMLDivElement>(null)
|
||||
const control = useRef<{ toggleCollapse: () => void } | null>(null)
|
||||
const placed = useRef(false)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const frame = frameRef.current
|
||||
// panelUtils has no teardown; attach once per element (StrictMode runs effects twice in development,
|
||||
// and two dblclick listeners would cancel each other out).
|
||||
if (!frame || frame.dataset.panelReady) return
|
||||
frame.dataset.panelReady = "1"
|
||||
control.current = makePanelDraggableAndCollapsible(frame, {
|
||||
handleEl: headerRef.current,
|
||||
bodyEl: bodyRef.current,
|
||||
onCollapseToggle: setCollapsed,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// First open: place the window inside the canvas stage, stacked from the right edge.
|
||||
useLayoutEffect(() => {
|
||||
const frame = frameRef.current
|
||||
const anchor = anchorRef.current
|
||||
if (!open || placed.current || !frame || !anchor) return
|
||||
placed.current = true
|
||||
const r = anchor.getBoundingClientRect()
|
||||
const w = frame.offsetWidth
|
||||
const left = Math.max(r.left + GAP, r.right - GAP - (slot + 1) * w - slot * GAP)
|
||||
frame.style.left = `${Math.round(left)}px`
|
||||
frame.style.top = `${Math.round(r.top + GAP)}px`
|
||||
}, [open, slot, anchorRef])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={frameRef}
|
||||
className="viewer-window"
|
||||
data-size={size}
|
||||
aria-label={title}
|
||||
style={{ display: open ? undefined : "none" }}
|
||||
>
|
||||
<Card size="sm" className="viewer-window-card">
|
||||
<CardHeader ref={headerRef} className="viewer-window-header border-b">
|
||||
<CardTitle className="flex min-w-0 items-center gap-2">
|
||||
<span aria-hidden="true" className="flex text-muted-foreground">{icon}</span>
|
||||
<span className="truncate">{title}</span>
|
||||
{meta}
|
||||
</CardTitle>
|
||||
<CardAction className="flex items-center gap-1">
|
||||
{actions}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={collapsed ? `${title} 펼치기` : `${title} 접기`}
|
||||
aria-expanded={!collapsed}
|
||||
onClick={() => control.current?.toggleCollapse()}
|
||||
>
|
||||
{collapsed ? <ChevronRightIcon aria-hidden="true" /> : <ChevronDownIcon aria-hidden="true" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{collapsed ? "펼치기" : "접기"} (제목 더블클릭)</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="icon-sm" aria-label={`${title} 닫기`} onClick={onClose}>
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>닫기</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<div ref={bodyRef} className="viewer-window-body">
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user