Files
dwg-dxf-viewer-sample/src/viewer2d/cadSpaces.ts
T

167 lines
5.4 KiB
TypeScript

/**
* Model Space / Paper Space (Layout) discovery for CadParseResult.
*
* DWG entities are owned by a block header handle. System blocks
* *Model_Space / *Paper_Space (and *Paper_Space0…) are the two spaces
* AutoCAD switches between. Viewer2D must render only one at a time —
* drawing both stacks survey-model geometry on top of layout sheet
* geometry and produces the "all text piled in the center" glitch.
*/
export type CadSpaceKind = 'model' | 'paper' | 'other';
export type CadSpace = {
/** Stable id for UI (hex handle string, same as ownerHandle.value.toString(16)). */
id: string;
/** Display label, e.g. "Model" / "Layout" / raw block name. */
label: string;
/** Raw block name from tables.blocks. */
name: string;
/** Block handle as number when available. */
handle: number | null;
/** Hex string matching entity ownerHandle.value.toString(16). */
handleHex: string;
kind: CadSpaceKind;
entityCount: number;
};
function handleToHex(handle: unknown): string | null {
if (handle == null) return null;
if (typeof handle === 'object' && handle !== null && 'value' in handle) {
const v = (handle as { value?: unknown }).value;
if (typeof v === 'number' && Number.isFinite(v)) return v.toString(16);
if (typeof v === 'string' && v.length) {
const n = Number(v);
return Number.isFinite(n) ? n.toString(16) : v.toLowerCase();
}
return null;
}
if (typeof handle === 'number' && Number.isFinite(handle)) return handle.toString(16);
if (typeof handle === 'string' && handle.length) {
const n = Number(handle);
return Number.isFinite(n) ? n.toString(16) : handle.toLowerCase();
}
return null;
}
function ownerHex(entity: { ownerHandle?: unknown }): string | null {
return handleToHex(entity.ownerHandle);
}
function classifySpaceName(raw: string): CadSpaceKind | null {
const name = raw.toLowerCase().replace(/\*/g, '').trim();
if (!name) return null;
if (name === 'model_space' || name === 'ms' || name.startsWith('model')) return 'model';
if (name === 'paper_space' || name === 'ps' || name.startsWith('paper')) return 'paper';
return null;
}
function spaceLabel(kind: CadSpaceKind, name: string, paperIndex: number): string {
if (kind === 'model') return 'Model';
if (kind === 'paper') {
// First paper space → "Layout"; further ones keep a disambiguator.
if (paperIndex <= 1) return 'Layout';
return `Layout (${name.replace(/^\*/, '')})`;
}
return name;
}
/**
* List model/paper space blocks that own at least zero entities.
* Always includes Model/Paper headers found in tables.blocks.
*/
export function listCadSpaces(result: {
entities?: unknown[];
tables?: { blocks?: Array<{ name?: string; handle?: unknown }> };
} | null | undefined): CadSpace[] {
const blocks = result?.tables?.blocks ?? [];
const entities = (result?.entities ?? []) as Array<{ ownerHandle?: unknown }>;
const counts = new Map<string, number>();
for (const e of entities) {
const hex = ownerHex(e);
if (!hex) continue;
counts.set(hex, (counts.get(hex) || 0) + 1);
}
const spaces: CadSpace[] = [];
let paperIndex = 0;
for (const b of blocks) {
const kind = classifySpaceName(b.name ?? '');
if (!kind) continue;
const hex = handleToHex(b.handle);
if (!hex) continue;
if (kind === 'paper') paperIndex += 1;
const handleNum =
typeof b.handle === 'number'
? b.handle
: typeof b.handle === 'object' && b.handle && 'value' in b.handle
? Number((b.handle as { value: unknown }).value)
: null;
spaces.push({
id: hex,
label: spaceLabel(kind, b.name ?? '', paperIndex),
name: b.name ?? '',
handle: Number.isFinite(handleNum as number) ? (handleNum as number) : null,
handleHex: hex,
kind,
entityCount: counts.get(hex) || 0,
});
}
// Stable order: Model first, then papers in table order.
spaces.sort((a, b) => {
if (a.kind !== b.kind) {
if (a.kind === 'model') return -1;
if (b.kind === 'model') return 1;
}
return 0;
});
return spaces;
}
/**
* Default tab when a file is opened:
* - Paper/Layout with entities if any (civil sheet drawings often leave Layout active)
* - else Model
* - else first listed space
*/
export function pickDefaultSpace(spaces: CadSpace[]): CadSpace | null {
if (!spaces.length) return null;
const paperWithContent = spaces.find((s) => s.kind === 'paper' && s.entityCount > 0);
if (paperWithContent) return paperWithContent;
const model = spaces.find((s) => s.kind === 'model');
if (model) return model;
return spaces[0] ?? null;
}
/**
* Owners allowed when filtering to one space: the space handle itself plus
* handles of entities that live under it (e.g. INSERT → ATTRIB children).
*/
export function buildAllowedOwnerHexes(
entities: Array<{ handle?: unknown; ownerHandle?: unknown }>,
activeSpaceHex: string,
rounds = 3,
): Set<string> {
const allowed = new Set<string>([activeSpaceHex]);
for (let pass = 0; pass < rounds; pass++) {
let grew = false;
for (const ent of entities) {
const oh = ownerHex(ent);
if (!oh || !allowed.has(oh)) continue;
const h = handleToHex(ent.handle);
if (h && !allowed.has(h)) {
allowed.add(h);
grew = true;
}
}
if (!grew) break;
}
return allowed;
}
export function isSystemSpaceBlockName(name: string): boolean {
return classifySpaceName(name) != null;
}