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,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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user