// Story fixture for SW/Viewer: the reference DWG/DXF viewer menu rebuilt from the public @hm-design/sw API only. // UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed. import { useMemo, useState } from "react" import { SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch, type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps, } from "@hm-design/sw" import { Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react" export const SAMPLES = [ { id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" }, { id: "sample-civil", label: "평면 및 종단면도 · DWG", name: "C0060202-001-평면및종단면도.dwg" }, { id: "sample-dxf", label: "기본 도형 · DXF", name: "simple.dxf" }, ] as const export const SPACES = [ { value: "model", label: "Model", count: 17377 }, { value: "layout1", label: "Layout", count: 237 }, ] export const LAYERS = [ { name: "0", color: "#ffffff", count: 1 }, { name: "CX-BORD-LIN1", color: "#00ffff", count: 30 }, { name: "CX-BORD-LIN2", color: "#00ff00", count: 1 }, { name: "CX-BORD-TEX3", color: "#ff00ff", count: 17 }, { name: "CX-BORD-TEX2", color: "#000000", count: 4 }, { name: "CX-MISC", color: "#ffff00", count: 0 }, { name: "CR-LEAT", color: "#00ff00", count: 4 }, { name: "CR-GRND", color: "#00ff00", count: 6 }, { name: "CR-TEXT", color: "#ff0000", count: 52 }, ] export type ViewerPanelId = "layers" | "options" | "properties" export interface ViewerOptions { lineweightOn: boolean; lineweightScale: number; grid: boolean; zoomSpeed: number; zScale: number } export const DEFAULT_OPTIONS: ViewerOptions = { lineweightOn: false, lineweightScale: 11, grid: false, zoomSpeed: 1, zScale: 10 } /** All UI state of the demo viewer screen + the header menu data built from it. */ export function useViewerDemo({ initialDocument = null as string | null, initialPanels = [] as ViewerPanelId[] } = {}) { const [documentName, setDocumentName] = useState(initialDocument) const [space, setSpace] = useState("model") const [is25D, set25D] = useState(false) const [dark, setDark] = useState(true) const [panels, setPanels] = useState>(() => ({ layers: initialPanels.includes("layers"), options: initialPanels.includes("options"), properties: initialPanels.includes("properties") })) const [options, setOptions] = useState(DEFAULT_OPTIONS) const [hidden, setHidden] = useState>(new Set()) const [aboutOpen, setAboutOpen] = useState(false) const [log, setLog] = useState([]) const record = (line: string) => setLog(lines => [...lines.slice(-7), line]) const toggle = (id: ViewerPanelId, next = !panels[id]) => setPanels(current => ({ ...current, [id]: next })) const loaded = documentName !== null // 2026-10-21: file open is the header button after the logo (SWHeader documentOpenPlacement="header"); the samples // are the file-name list (no Open / + New there). const openFile = () => record("파일 선택창 (미연결)") const documents = SAMPLES.map(sample => ({ id: sample.id, name: sample.name, title: sample.label })) const fileProps = { documents, activeDocumentId: SAMPLES.find(sample => sample.name === documentName)?.id, onDocumentSelect: (document: SWHeaderDocument) => { setDocumentName(document.name); record(`samples/${document.id}`) }, onDocumentOpen: openFile, documentOpenLabel: "파일 열기", } satisfies Partial const groups = useMemo(() => [ { id: "space", label: "도면 공간", surface: "field", hidden: !loaded, items: [ { type: "tabs", id: "space", label: "모델과 배치 선택", moreLabel: "도면 공간", collapseBelow: 1180, value: space, options: SPACES, onValueChange: setSpace }, ], }, { id: "view", label: "화면 보기", surface: "field", items: [ // 2026-10-21: shown while the header has room, icon only next, then into More (measured, see SWViewerHeader). { type: "button", id: "fit", label: "전체 보기", icon: , title: "전체 도면 맞춤 (F)", iconOnlyBelow: 560, collapseBelow: 1180, disabled: !loaded }, { type: "switch", id: "2.5d", label: "2.5D", checked: is25D, collapseBelow: 1180, onCheckedChange: set25D }, { type: "switch", id: "dark", label: "어두운 배경", checked: dark, collapseBelow: 1180, onCheckedChange: setDark }, ], }, { id: "panels", label: "패널", placement: "end", items: [ { type: "toggle", id: "layers", label: "레이어", icon: , title: "레이어 패널 열기 / 닫기", pressed: panels.layers, iconOnlyBelow: 900, onPressedChange: next => toggle("layers", next) }, { type: "toggle", id: "options", label: "설정", icon: , title: "옵션 패널 열기 / 닫기", pressed: panels.options, iconOnlyBelow: 900, onPressedChange: next => toggle("options", next) }, { type: "toggle", id: "properties", label: "속성", icon: , title: "속성 패널 열기 / 닫기", pressed: panels.properties, iconOnlyBelow: 900, onPressedChange: next => toggle("properties", next) }, ], }, ], [loaded, space, is25D, dark, panels]) /** Trailing slot: load state + the info button that opens the modal about popup (reference LicenseNotice). */ const trailing = ( <> } label="정보 및 라이선스" title="정보 및 라이선스" onClick={() => { setAboutOpen(true); record("about") }} /> ) const onCommand = (command: SWViewerCommand) => record(`${command.itemId}${command.id !== command.itemId ? `/${command.id}` : ""}${command.value !== undefined ? `=${String(command.value)}` : ""}`) return { documentName, setDocumentName, space, is25D, set25D, dark, setDark, panels, toggle, options, setOptions, hidden, setHidden, log, record, groups, trailing, onCommand, loaded, aboutOpen, setAboutOpen, fileProps, } } export type ViewerDemo = ReturnType type PanelPlacement = Pick export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelPlacement) { const [query, setQuery] = useState("") const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase())) const set = (names: string[]) => demo.setHidden(new Set(names)) // 2026-10-22: one switch for all layers. ON only when every layer is shown; a partial state reads OFF, so a click turns all on. const allVisible = demo.hidden.size === 0 return ( demo.toggle("layers", false)} {...placement}>
setQuery(event.currentTarget.value)} /> {/* 2026-10-22: the section title (.hm-sw-viewer-section__title) names the switch row; the switch stays on that line. */} { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />
{filtered.length === 0 ? 검색 결과가 없습니다 ("{query}") : (
    {filtered.map(layer => { const visible = !demo.hidden.has(layer.name) return (
  • ) })}
)}
) } function SliderRow({ label, unit, value, min, max, step, disabled, onChange }: { label: string; unit?: string; value: number; min: number; max: number; step: number; disabled?: boolean; onChange: (value: number) => void }) { return ( }> {id => { const next = Number(event.currentTarget.value); if (Number.isFinite(next)) onChange(next) }} />} ) } export function OptionsPanel({ demo, footer, ...placement }: { demo: ViewerDemo; footer?: boolean } & PanelPlacement) { const { options, setOptions } = demo const set = (patch: Partial) => setOptions({ ...options, ...patch }) return ( demo.toggle("options", false)} width={340} height={520} {...placement} headerActions={ { setOptions(DEFAULT_OPTIONS); demo.record("options:reset") }}>} footer={footer ? <> demo.toggle("options", false)}>취소 { demo.record("options:apply"); demo.toggle("options", false) }}>적용 : undefined}> {/* 2026-10-22 options image: LWDISPLAY toggle; scale input + px/mm unit on the label row, slider below. */} {id => set({ lineweightOn })} />} set({ lineweightScale })} /> 이 도면에는 굵은 선이 없습니다 (0.50mm였다면 {(0.5 * options.lineweightScale).toFixed(1)}px) AutoCAD "선가중치 설정 → 표시 배율"과 같습니다. 모델 공간의 선가중치는 줌과 무관한 화면 픽셀로 그려지므로 화면 DPI에 맞춰 조정합니다. {id => set({ grid })} />} set({ zoomSpeed })} /> {id => } set({ zScale })} /> 좌클릭 드래그로 회전, 중간/우클릭 드래그로 이동합니다. ) } const ENTITY_ROWS: [string, string][] = [["유형", "LINE"], ["레이어", "CX-BORD-LIN1"], ["색상", "ByLayer"], ["선종류", "Continuous"]] const GEOMETRY_ROWS: [string, string][] = [["시작 X", "18432.512"], ["시작 Y", "2210.004"], ["끝 X", "18992.040"], ["끝 Y", "2210.004"], ["길이", "559.528"]] export function PropertiesPanel({ demo, selected = true, ...placement }: { demo: ViewerDemo; selected?: boolean } & PanelPlacement) { const rows = (items: [string, string][]) => (
{items.map(([key, value]) =>
{key}
{value}
)}
) return ( demo.toggle("properties", false)} {...placement} headerActions={selected ? demo.record("zoomToSelection (미연결)")}> : undefined}> {selected ? <> LINE · h=2F1A{rows(ENTITY_ROWS)} {rows(GEOMETRY_ROWS)} : (
)}
) }