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:
minsungandClaude Opus 5.5 committed 2026-10-12 18:49:33 +09:00
1 parent 40befda905
commit aaa0972bc1
18 files changed
+2163

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
import { FileUpIcon, FolderOpenIcon } from "lucide-react"
import { Button, Card, CardContent, CardHeader, CardTitle, LoadingScreen, Progress } from "@hm-design/ui"
import { SAMPLE_DRAWINGS, type SampleDrawing } from "../lib/samples"
/** Shown on the empty canvas: the page's single brand action (open a file) plus the bundled samples. */
export function EmptyState({ onOpenFile, onSample }: { onOpenFile: () => void; onSample: (s: SampleDrawing) => void }) {
return (
<div className="viewer-stage-center">
<Card className="viewer-empty-card">
<CardHeader>
<CardTitle>도면을 열어 주세요</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<p className="text-muted-foreground">
DWG · DXF 파일을 선택하거나 이 화면에 끌어다 놓습니다. 주소에 <code>?model=경로</code>를 붙이면 자동으로 엽니다.
</p>
<Button type="button" size="lg" onClick={onOpenFile}>
<FolderOpenIcon aria-hidden="true" />
파일 열기
</Button>
<div className="grid gap-2">
<p className="viewer-section-title">샘플 도면</p>
<div className="flex flex-wrap gap-2">
{SAMPLE_DRAWINGS.map((s) => (
<Button key={s.id} type="button" variant="primary" appearance="outline" size="sm" title={s.hint} onClick={() => onSample(s)}>
{s.label}
</Button>
))}
</div>
</div>
</CardContent>
</Card>
</div>
)
}
export type LoadState = {
/** Text for the LoadingScreen label and the status line. */
label: string
/** 0–100 when measurable, null when not (no Content-Length). */
value: number | null
/** A stage is running (LoadingScreen shown); false only for the brief "done" frame of the Progress bar. */
busy: boolean
}
/**
* Load feedback, following HMDesign's split (loading-screen.md §2):
* - Progress (top edge of the canvas) — the measurable part: download bytes, then stage steps.
* - LoadingScreen (centre) — the unmeasurable part. WASM parsing and Viewer2D.load() block the main
* thread; LoadingScreen's spinner is a CSS opacity keyframe animation the compositor can keep running.
*/
export function LoadingOverlay({ state }: { state: LoadState }) {
return (
<>
{state.value != null ? (
<div className="viewer-stage-progress">
<Progress value={state.value} aria-label="도면 불러오기 진행률" />
</div>
) : null}
{state.busy ? (
<div className="viewer-stage-center viewer-stage-busy">
<LoadingScreen className="viewer-loading">{state.label}</LoadingScreen>
</div>
) : null}
</>
)
}
/** Drop target hint while a file is dragged over the window. */
export function DropOverlay() {
return (
<div className="viewer-stage-center viewer-drop" aria-hidden="true">
<div className="viewer-drop-box">
<FileUpIcon className="size-8" />
<span>여기에 놓아 도면 열기 (.dwg / .dxf)</span>
</div>
</div>
)
}