167 lines
5.4 KiB
TypeScript
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;
|
|
}
|