diff --git a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts index 842bca41..05c8fc68 100644 --- a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts +++ b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts @@ -91,10 +91,35 @@ export function resolveBoxBorders( }; } +/** 색 글을 비교용으로 정리 — 공백 · 대소문자 · `#` 3자리 · ARGB(`#FF000000`) · `rgb(0,0,0)` 를 `#rrggbb` 로. */ +function normalizeColor(c: string): string { + const t = c.trim().toLowerCase().replace(/\s+/g, ""); + const rgb = /^rgba?\((\d+),(\d+),(\d+)(?:,1(?:\.0+)?)?\)$/.exec(t); + if (rgb) + return `#${[rgb[1], rgb[2], rgb[3]].map((n) => Number(n).toString(16).padStart(2, "0")).join("")}`; + if (/^#[0-9a-f]{3}$/.test(t)) return `#${t[1]}${t[1]}${t[2]}${t[2]}${t[3]}${t[3]}`; + if (/^#ff[0-9a-f]{6}$/.test(t)) return `#${t.slice(3)}`; + return t; +} + +/** 엑셀 「자동」 글자색 · 테두리색 — 검정 · windowtext · auto 는 테마 글자색을 따름(저장 값은 그대로 · 그릴 때만). */ +export function isAutoInk(c: string | undefined): boolean { + if (!c) return true; + const n = normalizeColor(c); + return n === "#000000" || n === "black" || n === "windowtext" || n === "auto"; +} + +/** 엑셀 「자동」 채움 — 흰색 · window 는 채움 없음처럼(테마 바탕이 비침). */ +export function isAutoPaper(c: string | undefined): boolean { + if (!c) return true; + const n = normalizeColor(c); + return n === "#ffffff" || n === "white" || n === "window"; +} + /** 빈 글 = 인라인 테두리를 안 그림(css `aislo-grid--gridlines` 눈금선이 비쳐 보임). `scale` = 확대 · 축소 배율(선 굵기도 같이). */ function borderCss(side: BorderSide | undefined, scale: number): string { if (!side) return ""; - const color = side.색 ?? "currentColor"; + const color = isAutoInk(side.색) ? "var(--aislo-grid-fg)" : (side.색 as string); const w = (px: number): number => Math.max(1, Math.round(px * scale)); switch (side.선) { case "hair": @@ -147,8 +172,8 @@ export function applyCellStyle( s.textDecorationLine = [style.밑줄 && "underline", style.취소선 && "line-through"] .filter(Boolean) .join(" "); - s.color = style.글자색 ?? ""; - s.backgroundColor = style.채움 ?? ""; + s.color = isAutoInk(style.글자색) ? "" : (style.글자색 as string); + s.backgroundColor = isAutoPaper(style.채움) ? "" : (style.채움 as string); s.borderTop = borderCss(borders.위, scale); s.borderLeft = borderCss(borders.왼, scale); s.borderBottom = borderCss(borders.아래, scale); diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index e17fd02d..99811a22 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -16,6 +16,7 @@ import { colIndex, colName, parseA1, parseRange, toA1 } from "./spreadsheet_addr import { applyCellStyle, cellStyleAt, + isAutoInk, resolveBoxBorders, setCellText, type ResolvedBorders, @@ -460,7 +461,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { const display = cellDisplay(ctx, sheet, r0, c0, style.가로 ?? "general", style.형식); applyCellStyle(cellEl, style, borders, display.hAlign, zoom); // 조건부 서식 글자색이 이기고(엑셀과 같음) · 없으면 숫자 형식([Red] 등)이 정한 색. - if (display.color && !condStyle?.글자색) cellEl.style.color = display.color; + if (display.color && !condStyle?.글자색 && !isAutoInk(display.color)) + cellEl.style.color = display.color; const comment = sheet.comments?.[addr]; // 수는 칸 폭보다 길면 `#####`(엑셀과 같음) · 글은 지금처럼 줄임표. let text = display.text; diff --git a/B07_DesignDetail/openwebcad/src/App.consts.ts b/B07_DesignDetail/openwebcad/src/App.consts.ts index 22242cec..63613edb 100644 --- a/B07_DesignDetail/openwebcad/src/App.consts.ts +++ b/B07_DesignDetail/openwebcad/src/App.consts.ts @@ -176,8 +176,9 @@ export const CANVAS_INPUT_FIELD_HEIGHT = 20; /** * Canvas input field background color + * 자동색(검정) — 어두운 바탕 검정 칸에 흰 글 · 밝은 바탕 흰 칸에 검정 글(`theme.ts` paintColor) */ -export const CANVAS_INPUT_FIELD_BACKGROUND_COLOR = '#161616'; +export const CANVAS_INPUT_FIELD_BACKGROUND_COLOR = '#000000'; /** * Canvas input field text color diff --git a/B07_DesignDetail/openwebcad/src/App.css b/B07_DesignDetail/openwebcad/src/App.css index 62b2b8d4..84797833 100644 --- a/B07_DesignDetail/openwebcad/src/App.css +++ b/B07_DesignDetail/openwebcad/src/App.css @@ -151,6 +151,12 @@ body > canvas[data-id="canvas"] { border-bottom: 1px solid var(--cad-line); transition: height 120ms ease; } +/* 접힘 모드에서 탭으로 잠깐 펼침 — 도면 자리는 그대로 두고 위에 겹쳐 뜸(122px = 펼친 리본 높이) */ +.cad-ribbon[data-peek="true"] { + z-index: 10; + height: 122px; + box-shadow: var(--shadow-lg); +} .cad-ribbon-tabs { display: flex; gap: 2px; diff --git a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx index 426d4bbf..67e6dc26 100644 --- a/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx +++ b/B07_DesignDetail/openwebcad/src/components/Toolbar.tsx @@ -12,7 +12,7 @@ import { QuickAccessBar } from './QuickAccessBar'; import { Ribbon } from '../ribbon/Ribbon'; import { StatusBar } from './StatusBar'; import { useCadRefresh } from './use-cad-refresh'; -import { isRibbonCollapsed } from './ui-state'; +import { isInspectorCollapsed, isRibbonCollapsed, setInspectorCollapsed } from './ui-state'; import { getActiveToolActor } from '../state'; import type { Tool } from '../tools'; import { ViewControls } from './ViewControls'; @@ -20,11 +20,12 @@ import { DropHint } from './DropHint'; export const Toolbar: FC = () => { useCadRefresh(); - const [panelCollapsed, setPanelCollapsed] = useState(false); const [commandLineVisible, setCommandLineVisible] = useState(false); const activeTool = (getActiveToolActor()?.getSnapshot()?.context?.type ?? null) as Tool | null; const ribbonCollapsed = isRibbonCollapsed(); + // 좌측 패널 접힘 — 호스트(M02 미리보기)가 접힌 채로 실을 수 있게 ui-state 에 둔다 + const panelCollapsed = isInspectorCollapsed(); useEffect(() => { // 편 상태의 폭은 App.css의 `--cad-panel-width` 기본값을 쓴다. 여기 숫자를 박아 @@ -54,7 +55,7 @@ export const Toolbar: FC = () => { /> setPanelCollapsed((value) => !value)} + onToggleCollapsed={() => setInspectorCollapsed(!panelCollapsed)} /> diff --git a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts index 2afd797f..56343908 100644 --- a/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts +++ b/B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts @@ -242,7 +242,7 @@ export class ScreenCanvasDrawController implements DrawController { lineWidth: number, dash: number[] = [] ) { - // 저장된 도면 색은 다크 배경 기준 — 라이트 테마에서는 그릴 때만 대비를 맞춘다. + // 자동색(검정·흰색·기본색)은 바탕 따라 흰/검정 · 라이트 테마는 밝은 색 대비도 맞춤(그릴 때만). const color = paintColor(rawColor); if (this.batching) { const effectiveWidth = isHighlighted ? lineWidth + 1 : lineWidth; diff --git a/B07_DesignDetail/openwebcad/src/helpers/draw.ts b/B07_DesignDetail/openwebcad/src/helpers/draw.ts index 771cd8f8..861ae433 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/draw.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/draw.ts @@ -13,6 +13,7 @@ import { getShouldDrawCursor, getSnapPoint, getSnapPointOnAngleGuide, + isViewOnly, } from '../state'; import { drawCursor, @@ -65,7 +66,8 @@ export function draw(drawController: ScreenCanvasDrawController) { ); drawSnapPoint(drawController, closestSnapPoint, isMarked); - if (getShouldDrawCursor()) { + // 미리보기는 십자 커서 · 명령 안내 · 좌표 풍선을 그리지 않는다(손 모양 커서). + if (getShouldDrawCursor() && !isViewOnly()) { drawCursor(drawController); getInputController().draw(drawController); } diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index 710ddf7e..ae5fcdc7 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -42,6 +42,7 @@ import { getSnapPoint, getSnapPointOnAngleGuide, isHostSave, + isViewOnly, redo, setActiveToolActor, setAngleStep, @@ -111,6 +112,11 @@ export class InputController { canvas?.addEventListener('contextmenu', (evt) => { evt.preventDefault(); }); + // 가운데 단추 — 브라우저 자동 스크롤(페이지가 같이 움직임) · 붙여넣기를 막는다. + // 막는 자리는 mousedown(아래 handleMouseDown) — pointerdown 을 막으면 mousedown 이 안 와 팬이 끊긴다. + canvas?.addEventListener('auxclick', (evt: MouseEvent) => { + if (evt.button === MouseButton.Middle) evt.preventDefault(); + }); // 캔버스 밖(리본·패널) 위에서의 트랙패드 핀치도 브라우저를 확대시키지 않는다. // ctrl이 없는 휠은 그대로 둬서 패널 스크롤은 살린다. document.addEventListener( @@ -191,6 +197,10 @@ export class InputController { if (evt.button === MouseButton.Middle) { setPanStartLocation(null); } + if (evt.button === MouseButton.Left && isViewOnly()) { + setPanStartLocation(null); + return; + } if (evt.button === MouseButton.Left) { if (this.isSelectToolActive()) { // 선택 도구는 누를 때 첫 점을 이미 보냈다. 끌었으면 놓는 자리로 사각형을 닫는다. @@ -264,6 +274,9 @@ export class InputController { setPanStartLocation(newScreenMouseLocation); } + // 미리보기는 보기만 — 스냅 · 가까운 개체 강조를 하지 않는다. + if (isViewOnly()) return; + // Calculate angle guides and snap points if (getSnapEnabled()) { calculateAngleGuidesAndSnapPoints(); @@ -322,11 +335,18 @@ export class InputController { public handleMouseDown(evt: MouseEvent) { if (evt.button === MouseButton.Middle) { + evt.preventDefault(); // 자동 스크롤로 부모 페이지가 같이 굴러가던 것 setPanStartLocation(this.getCanvasPoint(evt)); return; } if (evt.button !== MouseButton.Left) return; if ((evt.target as HTMLElement | null)?.closest('.controls')) return; + if (isViewOnly()) { + // 미리보기 — 왼쪽 끌기는 선택 상자 대신 화면 옮기기. + evt.preventDefault(); + setPanStartLocation(this.getCanvasPoint(evt)); + return; + } // AutoCAD처럼 누른 자리에서 사각형이 시작되도록 선택 도구에만 첫 점을 미리 보낸다. // 다른 도구는 예전처럼 놓을 때 한 점을 받는다 (끌다가 점이 두 개 찍히지 않게). if (!this.isSelectToolActive()) return; diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index b42e3130..4fd4ad21 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -15,6 +15,7 @@ import { getScreenCanvasDrawController, isDrawingDirty, isDrawingReadOnly, + isViewOnly, resetUndoBaseline, setActiveLayerId, setDesignMeta, @@ -24,8 +25,9 @@ import { setHostSave, setHostTitle, setLayers, + setViewOnly, } from '../state.ts'; -import { setRibbonCollapsed } from '../components/ui-state.ts'; +import { setInspectorCollapsed, setRibbonCollapsed } from '../components/ui-state.ts'; import { toast } from 'react-toastify'; import { runCommandInput } from '../commands/run-command.ts'; import { setRecoveryScope } from '../helpers/autosave.ts'; @@ -60,6 +62,10 @@ interface DrawingLoadMessage { hostSave?: boolean; /** 리본을 접힌 채로 여는가 — 없으면(B07) 펼침 그대로. */ ribbonCollapsed?: boolean; + /** 좌측 패널(특성 · 도면층)을 접힌 채로 여는가 — 없으면(B07) 지금 상태 그대로. */ + panelsCollapsed?: boolean; + /** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */ + viewOnly?: boolean; } interface DrawingSaveRequestMessage { @@ -110,6 +116,10 @@ function registerTextDoubleClickEdit() { const canvas = getCanvas(); if (!canvas) return; canvas.addEventListener('dblclick', (event: MouseEvent) => { + if (isViewOnly()) { + getScreenCanvasDrawController().zoomToFitScreen(); + return; + } if (isDrawingReadOnly()) { toast.info('확정한 도면입니다. 고치려면 [수정]을 먼저 누르세요.'); return; @@ -211,6 +221,8 @@ export function registerAisloDrawingBridge() { setHostTitle(event.data.hostTitle ?? 'B07 상세 설계'); setHostSave(event.data.hostSave === true); setRibbonCollapsed(event.data.ribbonCollapsed === true); + if (event.data.panelsCollapsed) setInspectorCollapsed(true); + setViewOnly(event.data.viewOnly === true); if (event.data.frameEdit) applyFramePreview(); // 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도** // 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측: diff --git a/B07_DesignDetail/openwebcad/src/main.tsx b/B07_DesignDetail/openwebcad/src/main.tsx index 795a65e8..9a42948d 100644 --- a/B07_DesignDetail/openwebcad/src/main.tsx +++ b/B07_DesignDetail/openwebcad/src/main.tsx @@ -26,6 +26,7 @@ import { getLastDrawTimestamp, getScreenCanvasDrawController, getSnapPoint, + isViewOnly, setActiveLayerId, setActiveToolActor, setCanvas, @@ -83,7 +84,7 @@ function startDrawLoop( /** * Highlight the entity closest to the mouse when the select tool is active */ - if (getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT) { + if (getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT && !isViewOnly()) { const screenCanvasDrawController = getScreenCanvasDrawController(); if (!screenCanvasDrawController) { throw new Error('getScreenCanvasDrawController() returned null'); diff --git a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx index b46d3c77..4afe48c3 100644 --- a/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx +++ b/B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx @@ -153,6 +153,20 @@ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) = const activeTabId = getActiveRibbonTab(); const activeTab = RIBBON_TABS.find((tab) => tab.id === activeTabId) ?? RIBBON_TABS[0]; const collapsed = isRibbonCollapsed(); + // 접힘 모드에서 탭을 누르면 잠깐 펼침(도면 위에 겹쳐 뜸) — 리본 밖을 누르면 다시 접힘 + // (오피스 「리본 자동 숨기기」). 접기 단추로 펼치면 고정 펼침. + const [peek, setPeek] = useState(false); + const navRef = useRef(null); + const peeking = collapsed && peek; + + useEffect(() => { + if (!peeking) return; + const close = (event: PointerEvent) => { + if (!navRef.current?.contains(event.target as Node)) setPeek(false); + }; + window.addEventListener('pointerdown', close, true); + return () => window.removeEventListener('pointerdown', close, true); + }, [peeking]); const handleRun = (command: CadCommand) => { runCommand(command); @@ -160,7 +174,7 @@ export const Ribbon: FC = ({ activeTool, widgets, onCommandRun }) = }; return ( -