diff --git a/.github/workflows/public-ci.yml b/.github/workflows/public-ci.yml index c1a9ec674..c3eb87ee0 100644 --- a/.github/workflows/public-ci.yml +++ b/.github/workflows/public-ci.yml @@ -51,6 +51,9 @@ jobs: - name: Build source, components and demo run: pnpm build + - name: Verify CAD color mode integration + run: pnpm --filter @file-viewer/renderer-cad verify:color-mode + - name: Verify PPTX generated styles run: pnpm exec vitest run test/pptx-generated-style-regression.spec.ts diff --git a/packages/components/vue3/src/package/common/type.ts b/packages/components/vue3/src/package/common/type.ts index e73340882..ce5b37e67 100644 --- a/packages/components/vue3/src/package/common/type.ts +++ b/packages/components/vue3/src/package/common/type.ts @@ -2,6 +2,7 @@ import type { FileViewerAiOptions as CoreFileViewerAiOptions, FileViewerArchiveOptions as CoreFileViewerArchiveOptions, FileViewerBeforeOperation as CoreFileViewerBeforeOperation, + FileViewerCadColorMode as CoreFileViewerCadColorMode, FileViewerCadDwfLineWeightMode as CoreFileViewerCadDwfLineWeightMode, FileViewerCadOptions as CoreFileViewerCadOptions, FileViewerCadRenderer as CoreFileViewerCadRenderer, @@ -110,6 +111,7 @@ export type FileViewerSpreadsheetOptions = CoreFileViewerSpreadsheetOptions; export type FileViewerTypstOptions = CoreFileViewerTypstOptions; export type FileViewerCadRenderer = CoreFileViewerCadRenderer; +export type FileViewerCadColorMode = CoreFileViewerCadColorMode; export type FileViewerCadDwfLineWeightMode = CoreFileViewerCadDwfLineWeightMode; /** diff --git a/packages/components/vue3/src/package/index.ts b/packages/components/vue3/src/package/index.ts index dca994c4b..27acc31ce 100644 --- a/packages/components/vue3/src/package/index.ts +++ b/packages/components/vue3/src/package/index.ts @@ -107,6 +107,7 @@ export type { FileViewerApplyViewStateOptions, FileViewerArchiveOptions, FileViewerBeforeOperation, + FileViewerCadColorMode, FileViewerCadDwfLineWeightMode, FileViewerCadOptions, FileViewerCadRenderer, diff --git a/packages/core/src/contracts/types.ts b/packages/core/src/contracts/types.ts index 1aae40d9e..1c105723b 100644 --- a/packages/core/src/contracts/types.ts +++ b/packages/core/src/contracts/types.ts @@ -357,6 +357,8 @@ export type FileViewerMessageKey = | 'cad.toolbar.fit' | 'cad.toolbar.zoomOut' | 'cad.toolbar.zoomIn' + | 'cad.toolbar.colorSource' + | 'cad.toolbar.monochrome' | 'cad.layers.title' | 'cad.layers.count' | 'cad.layers.merged' @@ -1108,6 +1110,7 @@ export interface FileViewerDrawingOptions { export type FileViewerCadRenderer = 'auto' | 'webgl' | 'canvas2d'; export type FileViewerCadDwfLineWeightMode = 'adaptive' | 'physical' | 'hairline'; +export type FileViewerCadColorMode = 'source' | 'monochrome'; export type FileViewerCadFitMode = 'best' | 'native'; export interface FileViewerCadOptions { @@ -1118,6 +1121,12 @@ export interface FileViewerCadOptions { useWorker?: boolean; workerTimeoutMs?: number; renderer?: FileViewerCadRenderer; + /** Initial CAD color policy. `monochrome` applies one plot color without mutating source data. */ + colorMode?: FileViewerCadColorMode; + /** CSS color used by monochrome mode. Defaults to the CAD foreground color. */ + monochromeColor?: string; + /** Shows the CAD toolbar color-mode toggle. Defaults to true. */ + showColorModeToggle?: boolean; preferDwgWasm?: boolean; includePaperSpace?: boolean; maxInsertDepth?: number; diff --git a/packages/core/src/headless.ts b/packages/core/src/headless.ts index 2e63f6e27..f26c708cd 100644 --- a/packages/core/src/headless.ts +++ b/packages/core/src/headless.ts @@ -450,6 +450,7 @@ export type { FileViewerArchiveOptions, FileViewerApplyViewStateOptions, FileViewerBeforeOperation, + FileViewerCadColorMode, FileViewerCadDwfLineWeightMode, FileViewerCadOptions, FileViewerCadRenderer, diff --git a/packages/core/src/i18n/messages.de.ts b/packages/core/src/i18n/messages.de.ts index f74a5ca1c..3baf1d27f 100644 --- a/packages/core/src/i18n/messages.de.ts +++ b/packages/core/src/i18n/messages.de.ts @@ -348,6 +348,8 @@ export const DE_DE_MESSAGES: Record = { "cad.toolbar.fit": "Fit", "cad.toolbar.zoomOut": "Herauszoomen", "cad.toolbar.zoomIn": "Vergrößern", + "cad.toolbar.colorSource": "Originalfarben", + "cad.toolbar.monochrome": "Monochrom", "cad.layers.title": "Schichten", "cad.layers.count": "{count} Artikel", "cad.layers.merged": "{name} ({count} zusammengeführte Ebenen)", diff --git a/packages/core/src/i18n/messages.ja.ts b/packages/core/src/i18n/messages.ja.ts index be9197afb..5cbf7e2d2 100644 --- a/packages/core/src/i18n/messages.ja.ts +++ b/packages/core/src/i18n/messages.ja.ts @@ -349,6 +349,8 @@ export const JA_JP_MESSAGES: Record = { 'cad.toolbar.fit': '全体表示', 'cad.toolbar.zoomOut': '縮小', 'cad.toolbar.zoomIn': '拡大', + 'cad.toolbar.colorSource': '元の色', + 'cad.toolbar.monochrome': 'モノクロ', 'cad.layers.title': 'レイヤー', 'cad.layers.count': '{count} 件', 'cad.layers.merged': '{name}(同名レイヤー {count} 件を統合)', diff --git a/packages/core/src/i18n/messages.ts b/packages/core/src/i18n/messages.ts index fccd9f0f7..01a1658d2 100644 --- a/packages/core/src/i18n/messages.ts +++ b/packages/core/src/i18n/messages.ts @@ -367,6 +367,8 @@ const ZH_CN_MESSAGES: Record = { 'cad.toolbar.fit': '适配', 'cad.toolbar.zoomOut': '缩小', 'cad.toolbar.zoomIn': '放大', + 'cad.toolbar.colorSource': '彩色', + 'cad.toolbar.monochrome': '黑白', 'cad.layers.title': '图层', 'cad.layers.count': '{count} 项', 'cad.layers.merged': '{name}(已合并 {count} 个同名图层)', @@ -822,6 +824,8 @@ const EN_US_MESSAGES: Record = { 'cad.toolbar.fit': 'Fit', 'cad.toolbar.zoomOut': 'Zoom out', 'cad.toolbar.zoomIn': 'Zoom in', + 'cad.toolbar.colorSource': 'Source colors', + 'cad.toolbar.monochrome': 'Monochrome', 'cad.layers.title': 'Layers', 'cad.layers.count': '{count} items', 'cad.layers.merged': '{name} ({count} merged layers)', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3e294c561..c35f6f5e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1105,6 +1105,7 @@ export type { FileViewerArchiveOptions, FileViewerApplyViewStateOptions, FileViewerBeforeOperation, + FileViewerCadColorMode, FileViewerCadDwfLineWeightMode, FileViewerCadOptions, FileViewerCadRenderer, diff --git a/packages/renderers/cad/README.en.md b/packages/renderers/cad/README.en.md index 7a89141a8..bb7c28599 100644 --- a/packages/renderers/cad/README.en.md +++ b/packages/renderers/cad/README.en.md @@ -49,3 +49,17 @@ For private deployments, override them with `options.cad.wasmPath`, `options.cad ## Migration Note CAD preview has moved completely out of `@file-viewer/core`. Core now only keeps the asset manifest, shared types, and a compatibility error message, and it no longer installs `@flyfish-dev/cad-viewer` by default. Install this package and pass it through `renderers`, or use `@file-viewer/preset-all` for the complete CAD experience. + +## Monochrome plot mode + +Set `cad.colorMode` to `source` or `monochrome`. Monochrome rendering uses the CAD engine's entity-color policy rather than a CSS filter, keeping Canvas, WebGL, text overlays and native DWF output consistent while preserving alpha, line types, line weights and source data. + +```ts +{ + cad: { + colorMode: 'monochrome', + monochromeColor: '#000000', + showColorModeToggle: true, + }, +} +``` diff --git a/packages/renderers/cad/README.md b/packages/renderers/cad/README.md index f60b6d100..d6373bdf1 100644 --- a/packages/renderers/cad/README.md +++ b/packages/renderers/cad/README.md @@ -49,3 +49,17 @@ const options = { ## 迁移说明 CAD 预览已经从 `@file-viewer/core` 中彻底移出,core 只保留资产 manifest、类型和兼容错误提示,不再默认安装 `@flyfish-dev/cad-viewer`。完整 CAD 能力请安装本包并传入 `renderers`,或直接使用 `@file-viewer/preset-all`。 + +## 黑白出图模式 + +`cad.colorMode` 可设为 `source` 或 `monochrome`。黑白模式通过 CAD 引擎的实体颜色策略实现,不使用 CSS 滤镜,因此 Canvas、WebGL、文字覆盖层及原生 DWF 渲染可保持一致,同时保留透明度、线型、线宽和源文件数据。 + +```ts +{ + cad: { + colorMode: 'monochrome', + monochromeColor: '#000000', + showColorModeToggle: true, + }, +} +``` diff --git a/packages/renderers/cad/package.json b/packages/renderers/cad/package.json index b90e2104f..5501ffa0d 100644 --- a/packages/renderers/cad/package.json +++ b/packages/renderers/cad/package.json @@ -57,7 +57,8 @@ ], "scripts": { "build": "tsc -b tsconfig.json", - "type-check": "tsc -b tsconfig.json" + "type-check": "tsc -b tsconfig.json", + "verify:color-mode": "node scripts/verify-color-mode.mjs" }, "dependencies": { "@file-viewer/core": "workspace:3.0.0", diff --git a/packages/renderers/cad/scripts/verify-color-mode.mjs b/packages/renderers/cad/scripts/verify-color-mode.mjs new file mode 100644 index 000000000..9aaccd772 --- /dev/null +++ b/packages/renderers/cad/scripts/verify-color-mode.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; + +import { + applyCadViewerColorMode, + normalizeFileViewerCadColorMode, + resolveCadViewerSourceDocument, + resolveFileViewerCadMonochromeColor, + supportsCadViewerColorMode, +} from '../dist/colorMode.js'; + +assert.equal(normalizeFileViewerCadColorMode('monochrome'), 'monochrome'); +assert.equal(normalizeFileViewerCadColorMode('source'), 'source'); +assert.equal(normalizeFileViewerCadColorMode('unexpected'), 'source'); +assert.equal(resolveFileViewerCadMonochromeColor(' #101820 '), '#101820'); +assert.equal(resolveFileViewerCadMonochromeColor(undefined), '#000000'); + +const calls = []; +const viewer = { + setColorMode(mode, color) { + calls.push([mode, color]); + }, +}; + +assert.equal(supportsCadViewerColorMode(viewer), true); +assert.equal(applyCadViewerColorMode(viewer, 'monochrome', '#000000'), true); +assert.deepEqual(calls, [['monochrome', '#000000']]); +assert.equal(applyCadViewerColorMode({}, 'source', '#000000'), false); + +const parserDocument = { metadata: { parserOwned: true } }; +const renderDocument = { metadata: { renderOnly: true } }; +assert.equal(resolveCadViewerSourceDocument({ + getSourceDocument: () => parserDocument, + getDocument: () => renderDocument, +}), parserDocument); +assert.equal(resolveCadViewerSourceDocument({ + getDocument: () => renderDocument, +}), renderDocument); +assert.equal(resolveCadViewerSourceDocument(undefined), undefined); + +console.log('CAD color mode integration checks passed.'); diff --git a/packages/renderers/cad/src/cad.ts b/packages/renderers/cad/src/cad.ts index 072e21c69..bbae4fe61 100644 --- a/packages/renderers/cad/src/cad.ts +++ b/packages/renderers/cad/src/cad.ts @@ -31,6 +31,7 @@ import { unregisterFileViewerZoomProvider, type FileViewerApplyViewStateOptions, type FileRenderContext, + type FileViewerCadColorMode, type FileViewerCadOptions, type FileViewerFitRequest, type FileViewerFitResult, @@ -40,6 +41,13 @@ import { type FileViewerViewStateChangeSource, type FileViewerZoomState, } from '@file-viewer/core'; +import { + applyCadViewerColorMode, + normalizeFileViewerCadColorMode, + resolveFileViewerCadMonochromeColor, + resolveCadViewerSourceDocument, + supportsCadViewerColorMode, +} from './colorMode'; type CadStatus = 'loading' | 'ready' | 'error'; @@ -63,6 +71,9 @@ const cadStyle = ` .cad-tools,.cad-meta{display:flex;align-items:center;gap:8px} .cad-tools button{min-width:34px;min-height:30px;border:0;border-radius:8px;background:rgba(15,23,42,.06);color:#25344c;cursor:pointer;font-weight:800;letter-spacing:0;transition:background-color .18s ease,color .18s ease} .cad-tools button:hover{background:rgba(31,150,110,.14);color:#0f8f62} +.cad-tools button.cad-color-mode{min-width:52px;padding:0 10px;font-size:12px} +.cad-tools button.cad-color-mode[aria-pressed='true']{background:#0f172a;color:#fff} +.cad-tools button.cad-color-mode:disabled{cursor:not-allowed;opacity:.46} .cad-zoom,.cad-meta span{color:#64748b;font-size:12px;font-weight:800;letter-spacing:0} .cad-meta span{border-radius:999px;padding:5px 9px;background:rgba(15,23,42,.06)} .cad-body{display:grid;min-height:0;flex:1;grid-template-columns:minmax(176px,236px) minmax(0,1fr) minmax(142px,176px);background:#eef2f7} @@ -791,6 +802,8 @@ export default async function renderCad( const options: FileViewerCadOptions = context?.options?.cad || {}; const t = createFileViewerTranslator(context?.options); const locale = resolveFileViewerLocale(context?.options); + const monochromeColor = resolveFileViewerCadMonochromeColor(options.monochromeColor); + let colorMode = normalizeFileViewerCadColorMode(options.colorMode); let status: CadStatus = 'loading'; let progressMessage = t('cad.state.loadingViewer'); let errorMessage = ''; @@ -817,16 +830,18 @@ export default async function renderCad( const zoomOutButton = createElement('button', undefined, '-') as HTMLButtonElement; const zoomText = createElement('span', 'cad-zoom', '100%'); const zoomInButton = createElement('button', undefined, '+') as HTMLButtonElement; + const colorModeButton = createElement('button', 'cad-color-mode') as HTMLButtonElement; + colorModeButton.hidden = options.showColorModeToggle === false; const meta = createElement('div', 'cad-meta'); const typeMeta = createElement('span', undefined, normalizedType.toUpperCase()); const backendMeta = createElement('span', undefined, 'AUTO'); - [fitButton, zoomOutButton, zoomInButton].forEach(button => { + [fitButton, zoomOutButton, zoomInButton, colorModeButton].forEach(button => { button.type = 'button'; }); fitButton.title = t('cad.toolbar.fit'); zoomOutButton.title = t('cad.toolbar.zoomOut'); zoomInButton.title = t('cad.toolbar.zoomIn'); - tools.append(fitButton, zoomOutButton, zoomText, zoomInButton); + tools.append(fitButton, zoomOutButton, zoomText, zoomInButton, colorModeButton); meta.append(typeMeta, backendMeta); toolbar.append(tools, meta); @@ -905,6 +920,8 @@ export default async function renderCad( status, view: viewState ? { ...viewState } : null, backend: renderStats?.backend, + colorMode, + monochromeColor: colorMode === 'monochrome' ? monochromeColor : undefined, layerCount: layers.length, visibleLayers: layers .filter(layer => layer.isVisible !== false && !layer.isFrozen) @@ -1051,12 +1068,14 @@ export default async function renderCad( : layer.name; button.setAttribute('aria-pressed', String(layerVisible)); const color = createElement('span', 'cad-layer-color'); - if (typeof layer.color === 'string') { + if (colorMode === 'monochrome') { + color.style.background = monochromeColor; + } else if (typeof layer.color === 'string') { color.style.background = layer.color; } button.append(color, createElement('span', undefined, layer.name)); button.addEventListener('click', () => { - const document = viewer?.getDocument(); + const document = resolveCadViewerSourceDocument(viewer) as CadDocument | undefined; if (!document) { return; } @@ -1085,6 +1104,15 @@ export default async function renderCad( const syncState = () => { zoomText.textContent = `${getZoomPercent()}%`; backendMeta.textContent = (renderStats?.backend || 'auto').toUpperCase(); + const colorModeSupported = supportsCadViewerColorMode(viewer); + colorModeButton.disabled = status !== 'ready' || !colorModeSupported; + colorModeButton.setAttribute('aria-pressed', String(colorMode === 'monochrome')); + colorModeButton.textContent = t(colorMode === 'monochrome' + ? 'cad.toolbar.monochrome' + : 'cad.toolbar.colorSource'); + colorModeButton.title = t(colorMode === 'monochrome' + ? 'cad.toolbar.colorSource' + : 'cad.toolbar.monochrome'); state.hidden = status === 'ready'; state.classList.toggle('error', status === 'error'); if (status === 'loading') { @@ -1240,6 +1268,17 @@ export default async function renderCad( fitButton.addEventListener('click', () => fitToView()); zoomInButton.addEventListener('click', zoomIn); zoomOutButton.addEventListener('click', zoomOut); + colorModeButton.addEventListener('click', () => { + const previousMode = colorMode; + colorMode = colorMode === 'monochrome' ? 'source' : 'monochrome'; + if (!applyCadViewerColorMode(viewer, colorMode, monochromeColor)) { + colorMode = previousMode; + syncState(); + return; + } + syncUi(); + emitCadViewStateChange('cad-view-change', 'user'); + }); const createViewer = () => { const { wasmPath, workerUrl, dwfWasmUrl } = resolveFileViewerCadAssetUrls( @@ -1284,6 +1323,7 @@ export default async function renderCad( dwfMaxOverviewStrokeCssPx: options.dwfMaxOverviewStrokeCssPx, dwfMinTextCssPx: options.dwfMinTextCssPx, dwfMinFilledAreaCssPx: options.dwfMinFilledAreaCssPx, + ...({ colorMode, monochromeColor } as Record), autoFit: true, canvasOptions, onLoadProgress: updateProgress, diff --git a/packages/renderers/cad/src/colorMode.ts b/packages/renderers/cad/src/colorMode.ts new file mode 100644 index 000000000..7511ecd56 --- /dev/null +++ b/packages/renderers/cad/src/colorMode.ts @@ -0,0 +1,38 @@ +import type { FileViewerCadColorMode } from '@file-viewer/core'; + +export interface CadViewerColorModeAdapter { + setColorMode?: (mode: FileViewerCadColorMode, monochromeColor?: string) => void; + getColorMode?: () => FileViewerCadColorMode; + getMonochromeColor?: () => string | undefined; +} + +export function normalizeFileViewerCadColorMode(value: unknown): FileViewerCadColorMode { + return value === 'monochrome' ? 'monochrome' : 'source'; +} + +export function resolveFileViewerCadMonochromeColor(value: unknown, fallback = '#000000'): string { + if (typeof value === 'string' && value.trim()) return value.trim(); + return fallback; +} + +export function supportsCadViewerColorMode(value: unknown): value is CadViewerColorModeAdapter { + return Boolean(value && typeof (value as CadViewerColorModeAdapter).setColorMode === 'function'); +} + +export function resolveCadViewerSourceDocument(viewer: unknown): unknown { + const candidate = viewer as { + getSourceDocument?: () => unknown; + getDocument?: () => unknown; + } | null | undefined; + return candidate?.getSourceDocument?.() ?? candidate?.getDocument?.(); +} + +export function applyCadViewerColorMode( + viewer: unknown, + mode: FileViewerCadColorMode, + monochromeColor: string +): boolean { + if (!supportsCadViewerColorMode(viewer)) return false; + viewer.setColorMode?.(mode, monochromeColor); + return true; +} diff --git a/packages/renderers/cad/src/index.ts b/packages/renderers/cad/src/index.ts index c2a292797..1b03e5f86 100644 --- a/packages/renderers/cad/src/index.ts +++ b/packages/renderers/cad/src/index.ts @@ -6,6 +6,15 @@ import { type RendererDefinition, } from '@file-viewer/core'; +export { + applyCadViewerColorMode, + normalizeFileViewerCadColorMode, + resolveCadViewerSourceDocument, + resolveFileViewerCadMonochromeColor, + supportsCadViewerColorMode, +} from './colorMode'; +export type { CadViewerColorModeAdapter } from './colorMode'; + const cadDefinition = DEFAULT_RENDERER_DEFINITIONS.find( definition => definition.id === 'cad' ) as RendererDefinition | undefined;