/** * 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(); 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 { const allowed = new Set([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; }