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>
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
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>
|
||
)
|
||
}
|