Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.0
cache: npm
- run: npm ci
- run: npm run build
- run: node --test test/*.test.mjs
- run: npm run pack:dry
25 changes: 25 additions & 0 deletions docs/monochrome-color-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Monochrome CAD color mode

`CadViewer` can render authored CAD colors or apply one fixed plot color without rewriting parser-owned data.

```ts
import { CadViewer } from '@flyfish-dev/cad-viewer';

const viewer = new CadViewer({
container,
colorMode: 'monochrome',
monochromeColor: '#000000',
});

await viewer.loadFile(file);

// Switch at runtime while retaining the active pan/zoom view.
viewer.setColorMode('source');
viewer.setColorMode('monochrome', '#000000');
```

The policy is applied by the shared color resolver, so normalized DWG/DXF output is consistent across Canvas2D, retained WebGL geometry, text overlays, fills, layer colors and ByBlock inheritance. Source entity colors, line types, line weights, visibility, geometry and alpha values are preserved.

Native DWF/DWFx/XPS rendering forwards the same fixed color to `dwf-viewer` when the installed native renderer exposes `setMonochromeColor()`.

For lower-level rendering, use `createCadRenderDocument()` to attach a render-only policy to a shallow document view and pass that view to a renderer. `readCadColorPolicy()` inspects the active policy.
41 changes: 36 additions & 5 deletions src/core/color.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readCadColorPolicy, type CadColorMode } from './colorPolicy';
import type { CadDocument, CadEntity, CadLayer } from './types';

const BASE_ACI: Record<number, string> = {
Expand Down Expand Up @@ -30,7 +31,6 @@ const BASE_ACI: Record<number, string> = {
250: '#333333', 251: '#505050', 252: '#696969', 253: '#828282', 254: '#bebebe', 255: '#ffffff'
};


export type CadColorContrastMode = 'preserve' | 'adaptive';

export interface ColorResolveOptions {
Expand All @@ -39,6 +39,10 @@ export interface ColorResolveOptions {
trueColorByteOrder?: 'rgb' | 'bgr';
contrastMode?: CadColorContrastMode;
minColorContrast?: number;
/** Optional direct override; document render metadata is used when omitted. */
colorMode?: CadColorMode;
/** Fixed color used by monochrome mode. Defaults to the active foreground. */
monochromeColor?: string;
}

interface RgbaColor {
Expand Down Expand Up @@ -156,7 +160,7 @@ export function resolveCadColor(entity: CadEntity, document?: CadDocument, optio
color = resolveLayerColor(layer, options);
}

return adaptColorForCanvas(color ?? fallback, options);
return resolveCadRenderColor(color ?? fallback, document, options);
}

export function resolveFillColor(entity: CadEntity, document?: CadDocument, options: ColorResolveOptions = {}): string | undefined {
Expand All @@ -168,7 +172,35 @@ export function resolveFillColor(entity: CadEntity, document?: CadDocument, opti
color = Math.abs(n) <= 257 ? colorFromAci(n, options.foreground ?? '#ffffff', options.foreground ?? '#ffffff') : colorFromTrueColor(n, options.trueColorByteOrder ?? 'rgb');
}
if (!color && typeof entity.fillColorIndex === 'number') color = colorFromAci(entity.fillColorIndex, options.foreground ?? '#ffffff', options.foreground ?? '#ffffff');
return color ? adaptColorForCanvas(color, options) : undefined;
return color ? resolveCadRenderColor(color, document, options) : undefined;
}

/** Applies a monochrome plot-style override without changing source entities. */
export function applyCadColorPolicy(color: string, document?: CadDocument, options: ColorResolveOptions = {}): string {
const policy = readCadColorPolicy(document);
const mode = options.colorMode ?? policy.mode;
if (mode !== 'monochrome') return color;

const target = parseCssColor(options.monochromeColor ?? policy.monochromeColor ?? options.foreground ?? '#000000');
if (!target) return color;
const targetRgba = parseRgba(target);
if (!targetRgba) return target;
const sourceRgba = parseRgba(color);
return toCssColor({
r: targetRgba.r,
g: targetRgba.g,
b: targetRgba.b,
a: targetRgba.a * (sourceRgba?.a ?? 1)
});
}

function resolveCadRenderColor(color: string, document: CadDocument | undefined, options: ColorResolveOptions): string {
const policy = readCadColorPolicy(document);
const mode = options.colorMode ?? policy.mode;
const resolved = applyCadColorPolicy(color, document, options);
// A fixed plot color is deliberate; contrast adaptation must not replace it
// or discard authored transparency.
return mode === 'monochrome' ? resolved : adaptColorForCanvas(resolved, options);
}

export function adaptColorForCanvas(color: string, options: ColorResolveOptions = {}): string {
Expand Down Expand Up @@ -229,7 +261,6 @@ function resolveLayerColor(layer: CadLayer | undefined, options: ColorResolveOpt
return undefined;
}


export function isByBlockColor(entity: CadEntity): boolean {
const aci = firstNumber(entity.colorIndex, entity.colorNumber, (entity as Record<string, unknown>).aci);
if (aci === 0) return true;
Expand All @@ -238,7 +269,7 @@ export function isByBlockColor(entity: CadEntity): boolean {

export function applyByBlockColorInheritance(entity: CadEntity, parent: CadEntity, document?: CadDocument, options: ColorResolveOptions = {}): CadEntity {
if (!isByBlockColor(entity)) return entity;
const inheritedColor = resolveCadColor(parent, document, options);
const inheritedColor = resolveCadColor(parent, document, { ...options, colorMode: 'source' });
const clone: CadEntity = { ...entity, color: inheritedColor, trueColor: undefined, colorIndex: undefined, colorNumber: undefined };
return clone;
}
Expand Down
60 changes: 60 additions & 0 deletions src/core/colorPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { CadDocument } from './types';

export type CadColorMode = 'source' | 'monochrome';

export interface CadColorPolicy {
/** Preserve authored colors or replace renderable CAD colors with one fixed color. */
mode?: CadColorMode;
/** CSS color used when mode is `monochrome`. Defaults to the active renderer foreground. */
monochromeColor?: string;
}

const COLOR_MODE_KEY = 'cadColorMode';
const MONOCHROME_COLOR_KEY = 'cadMonochromeColor';

/**
* Creates a shallow render-only document view carrying a color policy in
* metadata. Geometry, layers, entities, parser data and the caller-owned
* document remain untouched.
*/
export function createCadRenderDocument(
document: CadDocument,
policy: CadColorPolicy = {}
): CadDocument {
const mode = policy.mode ?? 'source';
const current = readCadColorPolicy(document);
const nextColor = normalizeOptionalColor(policy.monochromeColor);

if (
current.mode === mode &&
current.monochromeColor === nextColor
) {
return document;
}

const metadata = { ...document.metadata };
if (mode === 'monochrome') {
metadata[COLOR_MODE_KEY] = mode;
if (nextColor) metadata[MONOCHROME_COLOR_KEY] = nextColor;
else delete metadata[MONOCHROME_COLOR_KEY];
} else {
delete metadata[COLOR_MODE_KEY];
delete metadata[MONOCHROME_COLOR_KEY];
}

return { ...document, metadata };
}

/** Reads the render-only color policy attached by createCadRenderDocument(). */
export function readCadColorPolicy(document?: CadDocument): Required<Pick<CadColorPolicy, 'mode'>> & Pick<CadColorPolicy, 'monochromeColor'> {
const metadata = document?.metadata;
const mode = metadata?.[COLOR_MODE_KEY] === 'monochrome' ? 'monochrome' : 'source';
const monochromeColor = normalizeOptionalColor(metadata?.[MONOCHROME_COLOR_KEY]);
return monochromeColor ? { mode, monochromeColor } : { mode };
}

function normalizeOptionalColor(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const normalized = value.trim();
return normalized || undefined;
}
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ export { extractCadBom, serializeCadBomCsv, serializeCadBomJson } from './core/b
export { computeCadDocumentBounds, resolveCadFitBounds, resolveCadSavedViewBounds } from './core/bounds';
export type { CadBoundsOptions } from './core/bounds';
export { createCadSceneDocument } from './core/scene';
export { createCadRenderDocument, readCadColorPolicy } from './core/colorPolicy';
export type { CadColorMode, CadColorPolicy } from './core/colorPolicy';
export { applyByBlockLineTypeInheritance, createDashedCadPrimitives, createDashedCadSegments, resolveCadLinePattern, resolveCadLineTypeReference, transformCadLineTypeGlyph } from './core/linetype';
export type { CadDashedPrimitives, CadLineTypeMarker, ResolvedCadLinePattern, ResolvedCadLinePatternRun } from './core/linetype';
export { CadShxFontRegistry } from './core/shx';
export type { CadShxGlyph, CadShxGlyphResolver } from './core/shx';
export { normalizeDwgDatabase } from './loaders/dwg/DwgParser';
export { detectCadFormat, readInputBytes } from './core/format';
export { isCadNativeRenderableLoader } from './core/types';
export { colorFromAci, colorFromTrueColor, resolveCadColor } from './core/color';
export { applyByBlockColorInheritance, applyCadColorPolicy, colorFromAci, colorFromTrueColor, resolveCadColor, resolveFillColor } from './core/color';
export type { CadColorContrastMode, ColorResolveOptions } from './core/color';

export type {
CadBlock,
Expand Down
31 changes: 27 additions & 4 deletions src/loaders/dwf/DwfLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { createCadDocument, flattenPages } from '../../core/entity';
import { detectCadFormat, extensionOf, readInputBytes } from '../../core/format';
import type { CadDocument, CadEntity, CadFormat, CadLoadInput, CadLoadOptions, CadLoadResult, CadNativeRenderableLoader, CadPage, CadPathCommand, CadPoint3D } from '../../core/types';

type CadColorLoadOptions = CadLoadOptions & {
colorMode?: 'source' | 'monochrome';
monochromeColor?: string;
canvasOptions?: { foreground?: string };
};

type DwfMonochromeOptions = {
monochromeColor?: string;
};

export class DwfLoader implements CadNativeRenderableLoader {
readonly id = 'dwf';
readonly label = 'DWF/DWFx native viewer powered by dwf-viewer';
Expand Down Expand Up @@ -107,6 +117,7 @@ export class DwfLoader implements CadNativeRenderableLoader {
setPreferWebgl?: (value: boolean) => void;
setPreferWasm?: (value: boolean) => void;
setLineWeightMode?: (value: NonNullable<CadLoadOptions['dwfLineWeightMode']>) => void;
setMonochromeColor?: (value?: string) => void;
minStrokeCssPx?: number;
maxOverviewStrokeCssPx?: number;
minTextCssPx?: number;
Expand All @@ -122,6 +133,7 @@ export class DwfLoader implements CadNativeRenderableLoader {
if (typeof options.dwfPreferWebgl === 'boolean') native.setPreferWebgl?.(options.dwfPreferWebgl);
if (typeof options.dwfPreferWasm === 'boolean') native.setPreferWasm?.(options.dwfPreferWasm);
if (options.dwfLineWeightMode) native.setLineWeightMode?.(options.dwfLineWeightMode);
native.setMonochromeColor?.(resolveDwfMonochromeColor(options));
setOptionalNumber(native, 'minStrokeCssPx', options.dwfMinStrokeCssPx);
setOptionalNumber(native, 'maxOverviewStrokeCssPx', options.dwfMaxOverviewStrokeCssPx);
setOptionalNumber(native, 'minTextCssPx', options.dwfMinTextCssPx);
Expand Down Expand Up @@ -278,15 +290,25 @@ function resolveDwfBackground(options: CadLoadOptions): string {
return canvasOptions?.background ?? '#05070d';
}

function resolveDwfMonochromeColor(options: CadLoadOptions): string | undefined {
const colorOptions = options as CadColorLoadOptions;
if (colorOptions.colorMode !== 'monochrome') return undefined;
const configured = colorOptions.monochromeColor?.trim() || colorOptions.canvasOptions?.foreground?.trim();
return configured || '#ffffff';
}

function buildDwfViewerOptions(options: CadLoadOptions, wasmUrl: string | undefined, background: string): DwfViewerOptions {
const monochromeColor = resolveDwfMonochromeColor(options);
return {
...buildDwfLoadOptions(options, wasmUrl, background),
maxDevicePixelRatio: options.dwfMaxDevicePixelRatio ?? 2,
maxCanvasPixels: options.dwfMaxCanvasPixels ?? 16_777_216
};
maxCanvasPixels: options.dwfMaxCanvasPixels ?? 16_777_216,
...(monochromeColor ? { monochromeColor } : {})
} as DwfViewerOptions & DwfMonochromeOptions;
}

function buildDwfLoadOptions(options: CadLoadOptions, wasmUrl: string | undefined, background: string): DwfLoadOptions {
const monochromeColor = resolveDwfMonochromeColor(options);
return {
wasmUrl,
preferWebgl: options.dwfPreferWebgl ?? true,
Expand All @@ -298,8 +320,9 @@ function buildDwfLoadOptions(options: CadLoadOptions, wasmUrl: string | undefine
minStrokeCssPx: options.dwfMinStrokeCssPx,
maxOverviewStrokeCssPx: options.dwfMaxOverviewStrokeCssPx,
minTextCssPx: options.dwfMinTextCssPx,
minFilledAreaCssPx: options.dwfMinFilledAreaCssPx
};
minFilledAreaCssPx: options.dwfMinFilledAreaCssPx,
...(monochromeColor ? { monochromeColor } : {})
} as DwfLoadOptions & DwfMonochromeOptions;
}

function setOptionalNumber<T extends object, K extends keyof T>(target: T, key: K, value: number | undefined): void {
Expand Down
Loading
Loading