@aspicio/core
Advanced tools
| //#region src/parse/errors.ts | ||
| /** | ||
| * The one parse-failure type, shared by every format (PARSE-12, PARSE-13). | ||
| * | ||
| * Messages are phrased for a person and shown directly by callers on every | ||
| * surface (viewer, API, MCP); a library's own internals never reach here. | ||
| * `format` names the parser that claimed a file and then rejected it, so a | ||
| * caller can report the culprit without matching on message text — it is | ||
| * undefined when no parser claimed the input at all. | ||
| */ | ||
| var DrawingParseError = class extends Error { | ||
| format; | ||
| constructor(message, format) { | ||
| super(message); | ||
| this.name = "DrawingParseError"; | ||
| this.format = format; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/geom/arc.ts | ||
| /** Number of polyline segments to approximate a sweep of `sweep` radians. */ | ||
| function segmentCount(sweep, curveSegments) { | ||
| const n = Math.ceil(Math.abs(sweep) / (2 * Math.PI) * curveSegments); | ||
| return Math.max(2, Math.min(n, 256)); | ||
| } | ||
| /** Sample an arc into points, inclusive of both endpoints. */ | ||
| function sampleArc(cx, cy, radius, startAngle, sweep, curveSegments) { | ||
| const n = segmentCount(sweep, curveSegments); | ||
| const points = []; | ||
| for (let i = 0; i <= n; i++) { | ||
| const a = startAngle + sweep * i / n; | ||
| points.push({ | ||
| x: cx + radius * Math.cos(a), | ||
| y: cy + radius * Math.sin(a) | ||
| }); | ||
| } | ||
| return points; | ||
| } | ||
| /** | ||
| * Expand a bulged polyline segment into arc points (excluding `p1`, | ||
| * including `p2`). Bulge = tan(sweep/4), negative for clockwise arcs. | ||
| * Same construction as ezdxf / three-dxf. | ||
| */ | ||
| function sampleBulge(p1, p2, bulge, curveSegments) { | ||
| const sweep = 4 * Math.atan(bulge); | ||
| const chord = Math.hypot(p2.x - p1.x, p2.y - p1.y); | ||
| if (chord < 1e-12 || Math.abs(sweep) < 1e-9) return [p2]; | ||
| const radius = chord / (2 * Math.sin(sweep / 2)); | ||
| const toCenter = Math.atan2(p2.y - p1.y, p2.x - p1.x) + Math.PI / 2 - sweep / 2; | ||
| const cx = p1.x + radius * Math.cos(toCenter); | ||
| const cy = p1.y + radius * Math.sin(toCenter); | ||
| const startAngle = Math.atan2(p1.y - cy, p1.x - cx); | ||
| const points = sampleArc(cx, cy, Math.abs(radius), startAngle, sweep, curveSegments); | ||
| points.shift(); | ||
| points[points.length - 1] = p2; | ||
| return points; | ||
| } | ||
| /** Sample an ellipse defined the DXF way (major axis vector + ratio). */ | ||
| function sampleEllipse(cx, cy, majorX, majorY, axisRatio, startParam, endParam, curveSegments) { | ||
| let sweep = endParam - startParam; | ||
| if (sweep <= 1e-9) sweep += 2 * Math.PI; | ||
| const n = segmentCount(sweep, curveSegments); | ||
| const minorX = -majorY * axisRatio; | ||
| const minorY = majorX * axisRatio; | ||
| const points = []; | ||
| for (let i = 0; i <= n; i++) { | ||
| const t = startParam + sweep * i / n; | ||
| const c = Math.cos(t); | ||
| const s = Math.sin(t); | ||
| points.push({ | ||
| x: cx + majorX * c + minorX * s, | ||
| y: cy + majorY * c + minorY * s | ||
| }); | ||
| } | ||
| return points; | ||
| } | ||
| //#endregion | ||
| export { DrawingParseError as i, sampleBulge as n, sampleEllipse as r, sampleArc as t }; |
| import { d as DrawingDocument, t as DrawingParser } from "./registry-CtIhdVSA.mjs"; | ||
| //#region src/parse/parse.d.ts | ||
| /** Parse DXF text into the normalized Aspicio document model. */ | ||
| declare function parseDxf(text: string): DrawingDocument; | ||
| /** | ||
| * Parse a DXF from raw bytes or text. Headless (no DOM/WebGL) — safe in Node | ||
| * and Cloudflare Workers. Use when the source arrives as bytes (e.g. a fetched | ||
| * file); pass a string to parse ASCII DXF text directly. | ||
| * | ||
| * Binary "AutoCAD Binary DXF" input (both the R12 1-byte and R13+ 2-byte code | ||
| * variants) is detected by its sentinel and decoded. Other bytes are decoded | ||
| * as UTF-8, which also covers ASCII; pre-2007 files using an ANSI code page | ||
| * ($DWGCODEPAGE) will decode non-ASCII text as U+FFFD. | ||
| */ | ||
| declare function parseDxfBytes(source: string | ArrayBuffer | Uint8Array): DrawingDocument; | ||
| /** | ||
| * True when `bytes` look like DXF — the registry's sniff for this format | ||
| * (PARSE-13). | ||
| * | ||
| * DXF text has no magic number, so the test is the shape of its first record: | ||
| * a group code on its own line. That accepts the two real-world openings (a | ||
| * `999` comment or `0`/`SECTION`) and rejects prose, markup, and other binary | ||
| * formats, which is the line PARSE-12 draws between "not a supported drawing | ||
| * file" and "not a valid DXF file". Binary DXF is claimed by its sentinel. | ||
| */ | ||
| declare function sniffDxf(bytes: Uint8Array): boolean; | ||
| //#endregion | ||
| //#region src/parse/binary.d.ts | ||
| /** | ||
| * Binary DXF support. | ||
| * | ||
| * A DXF file can be encoded as text (the usual group-code/value lines) or as | ||
| * "AutoCAD Binary DXF" — the same records packed as bytes behind a 22-byte | ||
| * sentinel. This module detects the binary form and transcodes it back into the | ||
| * canonical text stream, so the existing text parser handles it unchanged. | ||
| * | ||
| * Two on-disk variants exist and both are supported: | ||
| * - R13+ (AC1012 and later): group codes are 2-byte little-endian. | ||
| * - R12 and earlier: group codes are a single byte, with `0xFF` escaping to a | ||
| * following 2-byte code. | ||
| * The first record is always `0 SECTION`, so the byte after the first `0x00` | ||
| * code distinguishes them: `0x00` (a 2-byte code's high byte) vs. the `S` of | ||
| * "SECTION". | ||
| */ | ||
| /** True when `bytes` begin with the binary-DXF sentinel. */ | ||
| declare function isBinaryDxf(bytes: Uint8Array): boolean; | ||
| /** | ||
| * Transcode a binary DXF (per {@link isBinaryDxf}) into the equivalent | ||
| * group-code/value text that `parseDxf` consumes. Reads defensively: a | ||
| * truncated record ends the stream rather than throwing. | ||
| */ | ||
| declare function binaryDxfToText(bytes: Uint8Array): string; | ||
| //#endregion | ||
| //#region src/dxf.d.ts | ||
| /** The DXF parser, ready to pass to `parsers` (VIEW-15) or `parseWith`. */ | ||
| declare const dxfParser: DrawingParser; | ||
| //#endregion | ||
| export { binaryDxfToText, dxfParser, isBinaryDxf, parseDxf, parseDxfBytes, sniffDxf }; |
+857
| import { i as DrawingParseError, n as sampleBulge, t as sampleArc } from "./arc-CsclX-ZH.mjs"; | ||
| import { a as stripMText, n as unitLabel, r as decodeTextSpecials } from "./units-sMwLLkpb.mjs"; | ||
| import DxfParser from "dxf-parser"; | ||
| //#region src/parse/binary.ts | ||
| /** | ||
| * Binary DXF support. | ||
| * | ||
| * A DXF file can be encoded as text (the usual group-code/value lines) or as | ||
| * "AutoCAD Binary DXF" — the same records packed as bytes behind a 22-byte | ||
| * sentinel. This module detects the binary form and transcodes it back into the | ||
| * canonical text stream, so the existing text parser handles it unchanged. | ||
| * | ||
| * Two on-disk variants exist and both are supported: | ||
| * - R13+ (AC1012 and later): group codes are 2-byte little-endian. | ||
| * - R12 and earlier: group codes are a single byte, with `0xFF` escaping to a | ||
| * following 2-byte code. | ||
| * The first record is always `0 SECTION`, so the byte after the first `0x00` | ||
| * code distinguishes them: `0x00` (a 2-byte code's high byte) vs. the `S` of | ||
| * "SECTION". | ||
| */ | ||
| /** The 22-byte marker every binary DXF starts with. */ | ||
| const SENTINEL = "AutoCAD Binary DXF\r\n\0"; | ||
| const utf8 = new TextDecoder("utf-8"); | ||
| /** True when `bytes` begin with the binary-DXF sentinel. */ | ||
| function isBinaryDxf(bytes) { | ||
| if (bytes.length < 22) return false; | ||
| for (let i = 0; i < 22; i++) if (bytes[i] !== SENTINEL.charCodeAt(i)) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * The value width/type a group code carries in binary DXF, from the group-code | ||
| * ranges in the AutoCAD 2012 DXF reference. Wrong widths would desync the byte | ||
| * stream, so this table is validated end-to-end against real files. | ||
| */ | ||
| function valueKind(code) { | ||
| if (code <= 9) return "str"; | ||
| if (code <= 59) return "f64"; | ||
| if (code <= 79) return "i16"; | ||
| if (code <= 99) return "i32"; | ||
| if (code === 100 || code === 102 || code === 105) return "str"; | ||
| if (code >= 110 && code <= 149) return "f64"; | ||
| if (code >= 160 && code <= 169) return "i64"; | ||
| if (code >= 170 && code <= 179) return "i16"; | ||
| if (code >= 210 && code <= 239) return "f64"; | ||
| if (code >= 270 && code <= 289) return "i16"; | ||
| if (code >= 290 && code <= 299) return "bool"; | ||
| if (code >= 300 && code <= 309) return "str"; | ||
| if (code >= 310 && code <= 319) return "bin"; | ||
| if (code >= 320 && code <= 369) return "str"; | ||
| if (code >= 370 && code <= 389) return "i16"; | ||
| if (code >= 390 && code <= 399) return "str"; | ||
| if (code >= 400 && code <= 409) return "i16"; | ||
| if (code >= 410 && code <= 419) return "str"; | ||
| if (code >= 420 && code <= 429) return "i32"; | ||
| if (code >= 430 && code <= 439) return "str"; | ||
| if (code >= 440 && code <= 459) return "i32"; | ||
| if (code >= 460 && code <= 469) return "f64"; | ||
| if (code >= 470 && code <= 481) return "str"; | ||
| if (code === 999) return "str"; | ||
| if (code >= 1e3 && code <= 1009) return "str"; | ||
| if (code >= 1010 && code <= 1059) return "f64"; | ||
| if (code >= 1060 && code <= 1070) return "i16"; | ||
| if (code === 1071) return "i32"; | ||
| return "str"; | ||
| } | ||
| const HEX = "0123456789ABCDEF"; | ||
| /** | ||
| * Transcode a binary DXF (per {@link isBinaryDxf}) into the equivalent | ||
| * group-code/value text that `parseDxf` consumes. Reads defensively: a | ||
| * truncated record ends the stream rather than throwing. | ||
| */ | ||
| function binaryDxfToText(bytes) { | ||
| const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); | ||
| const end = bytes.length; | ||
| let p = 22; | ||
| const twoByte = bytes[p + 1] === 0; | ||
| const lines = []; | ||
| const readString = () => { | ||
| const start = p; | ||
| while (p < end && bytes[p] !== 0) p++; | ||
| if (p >= end) return null; | ||
| const text = utf8.decode(bytes.subarray(start, p)); | ||
| p++; | ||
| return text; | ||
| }; | ||
| while (p < end) { | ||
| let code; | ||
| if (twoByte) { | ||
| if (p + 2 > end) break; | ||
| code = view.getUint16(p, true); | ||
| p += 2; | ||
| } else { | ||
| code = bytes[p++]; | ||
| if (code === 255) { | ||
| if (p + 2 > end) break; | ||
| code = view.getUint16(p, true); | ||
| p += 2; | ||
| } | ||
| } | ||
| let value; | ||
| switch (valueKind(code)) { | ||
| case "str": { | ||
| const s = readString(); | ||
| if (s === null) break; | ||
| value = s; | ||
| break; | ||
| } | ||
| case "f64": | ||
| if (p + 8 > end) return lines.join("\n"); | ||
| value = String(view.getFloat64(p, true)); | ||
| p += 8; | ||
| break; | ||
| case "i16": | ||
| if (p + 2 > end) return lines.join("\n"); | ||
| value = String(view.getInt16(p, true)); | ||
| p += 2; | ||
| break; | ||
| case "i32": | ||
| if (p + 4 > end) return lines.join("\n"); | ||
| value = String(view.getInt32(p, true)); | ||
| p += 4; | ||
| break; | ||
| case "i64": | ||
| if (p + 8 > end) return lines.join("\n"); | ||
| value = String(view.getBigInt64(p, true)); | ||
| p += 8; | ||
| break; | ||
| case "bool": | ||
| if (p + 1 > end) return lines.join("\n"); | ||
| value = String(bytes[p]); | ||
| p += 1; | ||
| break; | ||
| case "bin": { | ||
| if (p + 1 > end) return lines.join("\n"); | ||
| const n = bytes[p++]; | ||
| if (p + n > end) return lines.join("\n"); | ||
| let hex = ""; | ||
| for (let i = 0; i < n; i++) { | ||
| const b = bytes[p + i]; | ||
| hex += HEX[b >> 4] + HEX[b & 15]; | ||
| } | ||
| p += n; | ||
| value = hex; | ||
| break; | ||
| } | ||
| } | ||
| if (value === void 0) break; | ||
| lines.push(String(code), value); | ||
| if (code === 0 && value === "EOF") break; | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| //#endregion | ||
| //#region src/parse/hatch.ts | ||
| const num$1 = (v) => typeof v === "number" ? v : Number(v); | ||
| function parseBoundaries(groups, start, count) { | ||
| const boundaries = []; | ||
| let i = start; | ||
| const at = (code) => i < groups.length && groups[i].code === code; | ||
| for (let p = 0; p < count && i < groups.length; p++) { | ||
| while (i < groups.length && groups[i].code !== 92) i++; | ||
| if (i >= groups.length) break; | ||
| const flag = num$1(groups[i].value); | ||
| i++; | ||
| if ((flag & 2) !== 0) { | ||
| let closed = false; | ||
| let numVerts = 0; | ||
| if (at(72)) i++; | ||
| if (at(73)) { | ||
| closed = num$1(groups[i].value) !== 0; | ||
| i++; | ||
| } | ||
| if (at(93)) { | ||
| numVerts = num$1(groups[i].value); | ||
| i++; | ||
| } | ||
| const vertices = []; | ||
| for (let v = 0; v < numVerts && i + 1 < groups.length; v++) { | ||
| const x = num$1(groups[i].value); | ||
| const y = num$1(groups[i + 1].value); | ||
| i += 2; | ||
| let bulge = 0; | ||
| if (at(42)) { | ||
| bulge = num$1(groups[i].value); | ||
| i++; | ||
| } | ||
| vertices.push({ | ||
| x, | ||
| y, | ||
| bulge | ||
| }); | ||
| } | ||
| boundaries.push({ | ||
| kind: "polyline", | ||
| closed, | ||
| vertices | ||
| }); | ||
| } else { | ||
| let numEdges = 0; | ||
| if (at(93)) { | ||
| numEdges = num$1(groups[i].value); | ||
| i++; | ||
| } | ||
| const edges = []; | ||
| for (let e = 0; e < numEdges && i < groups.length; e++) { | ||
| if (!at(72)) break; | ||
| const edgeType = num$1(groups[i].value); | ||
| i++; | ||
| const readVals = () => { | ||
| const vals = /* @__PURE__ */ new Map(); | ||
| while (i < groups.length && groups[i].code !== 72 && groups[i].code !== 92) { | ||
| const c = groups[i].code; | ||
| if (c === 97 || c === 75 || c === 76 || c === 98) break; | ||
| vals.set(c, num$1(groups[i].value)); | ||
| i++; | ||
| } | ||
| return vals; | ||
| }; | ||
| if (edgeType === 1) { | ||
| const vals = readVals(); | ||
| edges.push({ | ||
| type: "line", | ||
| x1: vals.get(10) ?? 0, | ||
| y1: vals.get(20) ?? 0, | ||
| x2: vals.get(11) ?? 0, | ||
| y2: vals.get(21) ?? 0 | ||
| }); | ||
| } else if (edgeType === 2) { | ||
| const vals = readVals(); | ||
| edges.push({ | ||
| type: "arc", | ||
| cx: vals.get(10) ?? 0, | ||
| cy: vals.get(20) ?? 0, | ||
| radius: vals.get(40) ?? 0, | ||
| start: (vals.get(50) ?? 0) * Math.PI / 180, | ||
| end: (vals.get(51) ?? 360) * Math.PI / 180, | ||
| ccw: (vals.get(73) ?? 1) !== 0 | ||
| }); | ||
| } else readVals(); | ||
| } | ||
| boundaries.push({ | ||
| kind: "edges", | ||
| edges | ||
| }); | ||
| } | ||
| } | ||
| return [boundaries, i]; | ||
| } | ||
| var HatchHandler = class { | ||
| ForEntityName = "HATCH"; | ||
| parseEntity(scanner, curr) { | ||
| const entity = { | ||
| type: String(curr.value), | ||
| solid: false, | ||
| boundaries: [] | ||
| }; | ||
| const groups = []; | ||
| let g = scanner.next(); | ||
| while (!scanner.isEOF() && g.code !== 0) { | ||
| groups.push({ | ||
| code: g.code, | ||
| value: g.value | ||
| }); | ||
| g = scanner.next(); | ||
| } | ||
| for (let i = 0; i < groups.length; i++) { | ||
| const { code, value } = groups[i]; | ||
| switch (code) { | ||
| case 8: | ||
| entity.layer = String(value); | ||
| break; | ||
| case 6: | ||
| entity.lineType = String(value); | ||
| break; | ||
| case 62: | ||
| entity.colorIndex = num$1(value); | ||
| break; | ||
| case 420: | ||
| entity.color = num$1(value); | ||
| break; | ||
| case 5: | ||
| entity.handle = value; | ||
| break; | ||
| case 2: | ||
| entity.solid = entity.solid || String(value).toUpperCase() === "SOLID"; | ||
| break; | ||
| case 70: | ||
| entity.solid = entity.solid || num$1(value) === 1; | ||
| break; | ||
| case 91: { | ||
| const [boundaries, next] = parseBoundaries(groups, i + 1, num$1(value)); | ||
| entity.boundaries = boundaries; | ||
| i = next - 1; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return entity; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/parse/viewport.ts | ||
| const num = (v) => typeof v === "number" ? v : Number(v); | ||
| var ViewportHandler = class { | ||
| ForEntityName = "VIEWPORT"; | ||
| parseEntity(scanner, _curr) { | ||
| const e = { | ||
| type: "VIEWPORT", | ||
| inPaperSpace: false, | ||
| id: 0, | ||
| centerX: 0, | ||
| centerY: 0, | ||
| width: 0, | ||
| height: 0, | ||
| viewCenterX: 0, | ||
| viewCenterY: 0, | ||
| viewHeight: 1, | ||
| twistDeg: 0 | ||
| }; | ||
| let g = scanner.next(); | ||
| while (!scanner.isEOF() && g.code !== 0) { | ||
| switch (g.code) { | ||
| case 8: | ||
| e.layer = String(g.value); | ||
| break; | ||
| case 67: | ||
| e.inPaperSpace = num(g.value) === 1; | ||
| break; | ||
| case 10: | ||
| e.centerX = num(g.value); | ||
| break; | ||
| case 20: | ||
| e.centerY = num(g.value); | ||
| break; | ||
| case 40: | ||
| e.width = num(g.value); | ||
| break; | ||
| case 41: | ||
| e.height = num(g.value); | ||
| break; | ||
| case 12: | ||
| e.viewCenterX = num(g.value); | ||
| break; | ||
| case 22: | ||
| e.viewCenterY = num(g.value); | ||
| break; | ||
| case 17: | ||
| e.viewTargetX = num(g.value); | ||
| break; | ||
| case 27: | ||
| e.viewTargetY = num(g.value); | ||
| break; | ||
| case 45: | ||
| e.viewHeight = num(g.value); | ||
| break; | ||
| case 51: | ||
| e.twistDeg = num(g.value); | ||
| break; | ||
| case 69: | ||
| e.id = num(g.value); | ||
| break; | ||
| } | ||
| g = scanner.next(); | ||
| } | ||
| return e; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/parse/parse.ts | ||
| const DEG2RAD = Math.PI / 180; | ||
| const DEFAULT_COLOR = 16777215; | ||
| /** | ||
| * Empty (or whitespace-only) input reads as empty; anything else is invalid. | ||
| * | ||
| * dxf-parser's own messages are library internals that mislead — "Empty file" | ||
| * fires for any single-line non-empty input, and "Unexpected end of input: EOF | ||
| * group not read…" is jargon — so we never surface them (PARSE-12). The error | ||
| * carries "dxf" as its format: this parser claimed the bytes and then rejected | ||
| * them, which is a different report than "no parser claimed it". | ||
| */ | ||
| function parseError(text) { | ||
| return new DrawingParseError(text.trim().length === 0 ? "The file is empty" : "Not a valid DXF file", "dxf"); | ||
| } | ||
| function point2(p) { | ||
| return { | ||
| x: p?.x ?? 0, | ||
| y: p?.y ?? 0 | ||
| }; | ||
| } | ||
| /** Entity color: explicit RGB, or null meaning ByLayer/ByBlock. */ | ||
| function entityColor(raw) { | ||
| if (raw.colorIndex === 0 || raw.colorIndex === 256) return null; | ||
| return typeof raw.color === "number" ? raw.color : null; | ||
| } | ||
| /** Linetype name, or undefined for ByLayer/continuous. */ | ||
| function lineTypeOf(raw) { | ||
| const name = raw.lineType; | ||
| if (!name || name === "ByLayer" || name === "BYLAYER") return void 0; | ||
| return name; | ||
| } | ||
| /** | ||
| * Lineweight in 1/100 mm (group 370), or undefined for the negative | ||
| * ByLayer/ByBlock/default codes (so the layer default applies). | ||
| */ | ||
| function lineWeightOf(raw) { | ||
| const w = raw.lineweight; | ||
| return typeof w === "number" && w >= 0 ? w : void 0; | ||
| } | ||
| /** | ||
| * OCS extrusion normal, or undefined for the default +Z. dxf-parser exposes | ||
| * it as separate fields (ARC, LWPOLYLINE) or a point (POLYLINE, INSERT). | ||
| * Note: dxf-parser does not parse 210 codes for CIRCLE — mirrored circles | ||
| * keep an unmirrored center until that upstream gap is fixed. | ||
| */ | ||
| function extrusionOf(e) { | ||
| const x = e.extrusionDirectionX ?? e.extrusionDirection?.x ?? 0; | ||
| const y = e.extrusionDirectionY ?? e.extrusionDirection?.y ?? 0; | ||
| const z = e.extrusionDirectionZ ?? e.extrusionDirection?.z ?? 1; | ||
| if (x === 0 && y === 0 && z === 1) return void 0; | ||
| return { | ||
| x, | ||
| y, | ||
| z | ||
| }; | ||
| } | ||
| function textHAlign(halign) { | ||
| if (halign === 1 || halign === 4) return "center"; | ||
| if (halign === 2) return "right"; | ||
| return "left"; | ||
| } | ||
| function textVAlign(valign) { | ||
| if (valign === 1) return "bottom"; | ||
| if (valign === 2) return "middle"; | ||
| if (valign === 3) return "top"; | ||
| return "baseline"; | ||
| } | ||
| function convertEntity(raw, unsupported) { | ||
| const base = { | ||
| layer: raw.layer ?? "0", | ||
| color: entityColor(raw), | ||
| lineType: lineTypeOf(raw), | ||
| lineWeight: lineWeightOf(raw) | ||
| }; | ||
| const e = raw; | ||
| switch (raw.type) { | ||
| case "LINE": { | ||
| const v = e.vertices ?? []; | ||
| if (v.length < 2) return null; | ||
| return { | ||
| ...base, | ||
| type: "LINE", | ||
| start: point2(v[0]), | ||
| end: point2(v[1]) | ||
| }; | ||
| } | ||
| case "LWPOLYLINE": | ||
| case "POLYLINE": { | ||
| const v = e.vertices ?? []; | ||
| if (v.length < 2) return null; | ||
| return { | ||
| ...base, | ||
| type: "POLYLINE", | ||
| extrusion: extrusionOf(e), | ||
| points: v.map(point2), | ||
| bulges: v.map((p) => p.bulge ?? 0), | ||
| closed: e.shape === true | ||
| }; | ||
| } | ||
| case "CIRCLE": return { | ||
| ...base, | ||
| type: "CIRCLE", | ||
| center: point2(e.center), | ||
| radius: e.radius ?? 0 | ||
| }; | ||
| case "ARC": return { | ||
| ...base, | ||
| type: "ARC", | ||
| extrusion: extrusionOf(e), | ||
| center: point2(e.center), | ||
| radius: e.radius ?? 0, | ||
| startAngle: e.startAngle ?? 0, | ||
| endAngle: e.endAngle ?? 0 | ||
| }; | ||
| case "ELLIPSE": return { | ||
| ...base, | ||
| type: "ELLIPSE", | ||
| center: point2(e.center), | ||
| majorAxis: point2(e.majorAxisEndPoint), | ||
| axisRatio: e.axisRatio ?? 1, | ||
| startParam: e.startAngle ?? 0, | ||
| endParam: e.endAngle ?? 2 * Math.PI | ||
| }; | ||
| case "INSERT": | ||
| if (!e.name) return null; | ||
| return { | ||
| ...base, | ||
| type: "INSERT", | ||
| extrusion: extrusionOf(e), | ||
| blockName: e.name, | ||
| position: point2(e.position), | ||
| scale: { | ||
| x: e.xScale ?? 1, | ||
| y: e.yScale ?? 1 | ||
| }, | ||
| rotation: (e.rotation ?? 0) * DEG2RAD | ||
| }; | ||
| case "TEXT": { | ||
| const text = decodeTextSpecials(e.text ?? ""); | ||
| if (!text) return null; | ||
| const halign = e.halign ?? 0; | ||
| const valign = e.valign ?? 0; | ||
| const position = (halign !== 0 || valign !== 0) && e.endPoint ? point2(e.endPoint) : point2(e.startPoint); | ||
| return { | ||
| ...base, | ||
| type: "TEXT", | ||
| position, | ||
| text, | ||
| height: e.textHeight ?? 1, | ||
| rotation: (e.rotation ?? 0) * DEG2RAD, | ||
| widthFactor: e.xScale ?? 1, | ||
| hAlign: textHAlign(halign), | ||
| vAlign: textVAlign(valign) | ||
| }; | ||
| } | ||
| case "MTEXT": { | ||
| const text = decodeTextSpecials(stripMText(e.text ?? "")); | ||
| if (!text) return null; | ||
| const ap = e.attachmentPoint ?? 1; | ||
| const hCol = (ap - 1) % 3; | ||
| const vRow = Math.floor((ap - 1) / 3); | ||
| const rotation = typeof e.rotation === "number" ? e.rotation * DEG2RAD : e.directionVector ? Math.atan2(e.directionVector.y ?? 0, e.directionVector.x ?? 1) : 0; | ||
| return { | ||
| ...base, | ||
| type: "TEXT", | ||
| position: point2(e.position), | ||
| text, | ||
| height: e.height ?? 1, | ||
| rotation, | ||
| widthFactor: 1, | ||
| hAlign: hCol === 1 ? "center" : hCol === 2 ? "right" : "left", | ||
| vAlign: vRow === 0 ? "top" : vRow === 1 ? "middle" : "bottom" | ||
| }; | ||
| } | ||
| case "SPLINE": { | ||
| const controlPoints = (e.controlPoints ?? []).map(point2); | ||
| if (controlPoints.length < 2) return null; | ||
| return { | ||
| ...base, | ||
| type: "SPLINE", | ||
| controlPoints, | ||
| knots: Array.isArray(e.knotValues) ? e.knotValues : [], | ||
| degree: e.degreeOfSplineCurve ?? 3, | ||
| closed: e.closed === true | ||
| }; | ||
| } | ||
| case "SOLID": | ||
| case "TRACE": { | ||
| const pts = (e.points ?? []).map(point2); | ||
| if (pts.length < 3) return null; | ||
| const points = pts.length >= 4 ? [ | ||
| pts[0], | ||
| pts[1], | ||
| pts[3], | ||
| pts[2] | ||
| ] : pts; | ||
| return { | ||
| ...base, | ||
| type: "SOLID", | ||
| points | ||
| }; | ||
| } | ||
| case "3DFACE": { | ||
| const v = (e.vertices ?? []).map(point2); | ||
| if (v.length < 3) return null; | ||
| return { | ||
| ...base, | ||
| type: "SOLID", | ||
| points: v | ||
| }; | ||
| } | ||
| case "POINT": return { | ||
| ...base, | ||
| type: "POINT", | ||
| position: point2(e.position) | ||
| }; | ||
| case "HATCH": return convertHatch(raw, base); | ||
| case "DIMENSION": | ||
| if (!e.block) return null; | ||
| return { | ||
| ...base, | ||
| type: "DIMENSION", | ||
| block: e.block, | ||
| position: point2(e.anchorPoint ?? e.insertionPoint) | ||
| }; | ||
| default: | ||
| unsupported[raw.type] = (unsupported[raw.type] ?? 0) + 1; | ||
| return null; | ||
| } | ||
| } | ||
| /** Sample one HATCH boundary loop into a closed polyline. */ | ||
| function sampleBoundary(b) { | ||
| if (b.kind === "polyline") { | ||
| const v = b.vertices; | ||
| if (v.length < 2) return []; | ||
| const out = [v[0]]; | ||
| const last = b.closed ? v.length : v.length - 1; | ||
| for (let i = 0; i < last; i++) { | ||
| const p1 = v[i]; | ||
| const p2 = v[(i + 1) % v.length]; | ||
| if (p1.bulge) out.push(...sampleBulge(p1, p2, p1.bulge, 72)); | ||
| else out.push(p2); | ||
| } | ||
| return out; | ||
| } | ||
| const out = []; | ||
| for (const e of b.edges) if (e.type === "line") out.push({ | ||
| x: e.x1, | ||
| y: e.y1 | ||
| }, { | ||
| x: e.x2, | ||
| y: e.y2 | ||
| }); | ||
| else { | ||
| let sweep = e.end - e.start; | ||
| if (!e.ccw) sweep = -Math.abs(sweep === 0 ? 2 * Math.PI : sweep); | ||
| else if (sweep <= 1e-9) sweep += 2 * Math.PI; | ||
| out.push(...sampleArc(e.cx, e.cy, e.radius, e.start, sweep, 72)); | ||
| } | ||
| return out; | ||
| } | ||
| function convertHatch(raw, base) { | ||
| const loops = raw.boundaries.map(sampleBoundary).filter((loop) => loop.length >= 3); | ||
| if (loops.length === 0) return null; | ||
| return { | ||
| ...base, | ||
| type: "HATCH", | ||
| loops, | ||
| solid: raw.solid | ||
| }; | ||
| } | ||
| /** Convert a raw VIEWPORT to a model Viewport, or null if it isn't a window. */ | ||
| function convertViewport(v) { | ||
| if (v.id === 1 || v.width <= 0 || v.height <= 0 || v.viewHeight <= 0) return null; | ||
| const viewCenter = v.viewTargetX !== void 0 && v.viewTargetY !== void 0 ? { | ||
| x: v.viewTargetX, | ||
| y: v.viewTargetY | ||
| } : { | ||
| x: v.viewCenterX, | ||
| y: v.viewCenterY | ||
| }; | ||
| return { | ||
| center: { | ||
| x: v.centerX, | ||
| y: v.centerY | ||
| }, | ||
| width: v.width, | ||
| height: v.height, | ||
| viewCenter, | ||
| viewHeight: v.viewHeight, | ||
| twist: v.twistDeg * Math.PI / 180 | ||
| }; | ||
| } | ||
| /** | ||
| * Assemble paper-space layouts: the active layout (from the ENTITIES section) | ||
| * first, then any `*Paper_Space<N>` blocks. Names are generic for now — real | ||
| * names live in the OBJECTS section, which dxf-parser doesn't expose. | ||
| */ | ||
| function buildLayouts(activeEntities, activeViewports, blocks, blockViewports) { | ||
| const layouts = []; | ||
| if (activeEntities.length > 0 || activeViewports.length > 0) layouts.push({ | ||
| name: "Layout1", | ||
| entities: activeEntities, | ||
| viewports: activeViewports | ||
| }); | ||
| const others = [...blocks.values()].filter((b) => /^\*Paper_Space\d+$/i.test(b.name)).sort((a, b) => a.name.localeCompare(b.name)); | ||
| for (const block of others) { | ||
| const viewports = blockViewports.get(block.name) ?? []; | ||
| if (block.entities.length === 0 && viewports.length === 0) continue; | ||
| layouts.push({ | ||
| name: `Layout${layouts.length + 1}`, | ||
| entities: block.entities, | ||
| viewports | ||
| }); | ||
| } | ||
| return layouts; | ||
| } | ||
| function parseLineTypes(dxf) { | ||
| const map = /* @__PURE__ */ new Map(); | ||
| const raw = dxf.tables?.lineType?.lineTypes ?? {}; | ||
| for (const [name, def] of Object.entries(raw)) { | ||
| const pattern = (def.pattern ?? []).map((n) => typeof n === "number" ? n : Number(n)).filter((n) => Number.isFinite(n)); | ||
| const patternLength = pattern.reduce((sum, n) => sum + Math.abs(n), 0); | ||
| map.set(name, { | ||
| name, | ||
| pattern, | ||
| patternLength | ||
| }); | ||
| } | ||
| return map; | ||
| } | ||
| /** | ||
| * Coerce out-of-range boolean group values (codes 290–299) to 0/1. | ||
| * Real-world files carry e.g. `$XCLIPFRAME 290 2` (a 0/1/2 enum since DXF | ||
| * 2010), which dxf-parser's scanner refuses to cast to boolean (PARSE-11). | ||
| */ | ||
| function coerceBooleanGroups(text) { | ||
| const lines = text.split(/\r\n|\r|\n/); | ||
| for (let i = 0; i + 1 < lines.length; i += 2) { | ||
| const code = Number(lines[i]); | ||
| if (code >= 290 && code <= 299) { | ||
| const value = lines[i + 1].trim(); | ||
| if (value !== "0" && value !== "1") lines[i + 1] = Number(value) ? "1" : "0"; | ||
| } | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function runParser(text) { | ||
| const parser = new DxfParser(); | ||
| const register = parser.registerEntityHandler.bind(parser); | ||
| register(HatchHandler); | ||
| register(ViewportHandler); | ||
| return parser.parseSync(text); | ||
| } | ||
| /** Parse DXF text into the normalized Aspicio document model. */ | ||
| function parseDxf(text) { | ||
| let dxf; | ||
| try { | ||
| dxf = runParser(text); | ||
| } catch (err) { | ||
| if (!(err instanceof TypeError && /cast to Boolean/.test(err.message))) throw parseError(text); | ||
| try { | ||
| dxf = runParser(coerceBooleanGroups(text)); | ||
| } catch { | ||
| throw parseError(text); | ||
| } | ||
| } | ||
| if (!dxf) throw parseError(text); | ||
| const unsupported = {}; | ||
| const lineTypes = parseLineTypes(dxf); | ||
| const layers = /* @__PURE__ */ new Map(); | ||
| const rawLayers = dxf.tables?.layer?.layers ?? {}; | ||
| for (const [name, layer] of Object.entries(rawLayers)) layers.set(name, { | ||
| name, | ||
| color: typeof layer.color === "number" ? layer.color : DEFAULT_COLOR, | ||
| visible: layer.visible !== false && layer.frozen !== true, | ||
| frozen: layer.frozen === true, | ||
| entityCount: 0, | ||
| lineType: layer.lineType, | ||
| lineWeight: lineWeightOf(layer) | ||
| }); | ||
| const ensureLayer = (name) => { | ||
| let layer = layers.get(name); | ||
| if (!layer) { | ||
| layer = { | ||
| name, | ||
| color: DEFAULT_COLOR, | ||
| visible: true, | ||
| frozen: false, | ||
| entityCount: 0 | ||
| }; | ||
| layers.set(name, layer); | ||
| } | ||
| return layer; | ||
| }; | ||
| const entities = []; | ||
| const paperEntities = []; | ||
| const paperViewports = []; | ||
| for (const raw of dxf.entities ?? []) { | ||
| if (raw.type === "VIEWPORT") { | ||
| const vp = convertViewport(raw); | ||
| if (vp) paperViewports.push(vp); | ||
| continue; | ||
| } | ||
| const entity = convertEntity(raw, unsupported); | ||
| if (!entity) continue; | ||
| ensureLayer(entity.layer).entityCount += 1; | ||
| if (raw.inPaperSpace) paperEntities.push(entity); | ||
| else entities.push(entity); | ||
| } | ||
| const blocks = /* @__PURE__ */ new Map(); | ||
| const blockViewports = /* @__PURE__ */ new Map(); | ||
| const rawBlocks = dxf.blocks ?? {}; | ||
| for (const [name, block] of Object.entries(rawBlocks)) { | ||
| const blockEntities = []; | ||
| const viewports = []; | ||
| for (const raw of block.entities ?? []) { | ||
| if (raw.type === "VIEWPORT") { | ||
| const vp = convertViewport(raw); | ||
| if (vp) viewports.push(vp); | ||
| continue; | ||
| } | ||
| const entity = convertEntity(raw, unsupported); | ||
| if (entity) { | ||
| ensureLayer(entity.layer); | ||
| blockEntities.push(entity); | ||
| } | ||
| } | ||
| blocks.set(name, { | ||
| name, | ||
| basePoint: point2(block.position), | ||
| entities: blockEntities | ||
| }); | ||
| if (viewports.length > 0) blockViewports.set(name, viewports); | ||
| } | ||
| const layouts = buildLayouts(paperEntities, paperViewports, blocks, blockViewports); | ||
| const insunits = dxf.header?.["$INSUNITS"]; | ||
| return { | ||
| layers, | ||
| entities, | ||
| blocks, | ||
| lineTypes, | ||
| unsupported, | ||
| units: unitLabel(typeof insunits === "number" ? insunits : void 0), | ||
| layouts, | ||
| format: "dxf" | ||
| }; | ||
| } | ||
| /** | ||
| * Parse a DXF from raw bytes or text. Headless (no DOM/WebGL) — safe in Node | ||
| * and Cloudflare Workers. Use when the source arrives as bytes (e.g. a fetched | ||
| * file); pass a string to parse ASCII DXF text directly. | ||
| * | ||
| * Binary "AutoCAD Binary DXF" input (both the R12 1-byte and R13+ 2-byte code | ||
| * variants) is detected by its sentinel and decoded. Other bytes are decoded | ||
| * as UTF-8, which also covers ASCII; pre-2007 files using an ANSI code page | ||
| * ($DWGCODEPAGE) will decode non-ASCII text as U+FFFD. | ||
| */ | ||
| function parseDxfBytes(source) { | ||
| if (typeof source === "string") return parseDxf(source); | ||
| const bytes = source instanceof Uint8Array ? source : new Uint8Array(source); | ||
| return parseDxf(isBinaryDxf(bytes) ? binaryDxfToText(bytes) : new TextDecoder().decode(bytes)); | ||
| } | ||
| /** | ||
| * True when `bytes` look like DXF — the registry's sniff for this format | ||
| * (PARSE-13). | ||
| * | ||
| * DXF text has no magic number, so the test is the shape of its first record: | ||
| * a group code on its own line. That accepts the two real-world openings (a | ||
| * `999` comment or `0`/`SECTION`) and rejects prose, markup, and other binary | ||
| * formats, which is the line PARSE-12 draws between "not a supported drawing | ||
| * file" and "not a valid DXF file". Binary DXF is claimed by its sentinel. | ||
| */ | ||
| function sniffDxf(bytes) { | ||
| if (isBinaryDxf(bytes)) return true; | ||
| const head = new TextDecoder().decode(bytes.subarray(0, 64)); | ||
| return /^\uFEFF?(?:[ \t]*\r?\n)*[ \t]*\d{1,4}[ \t]*\r?\n/.test(head); | ||
| } | ||
| //#endregion | ||
| //#region src/dxf.ts | ||
| /** The DXF parser, ready to pass to `parsers` (VIEW-15) or `parseWith`. */ | ||
| const dxfParser = { | ||
| format: "dxf", | ||
| sniff: sniffDxf, | ||
| parse: parseDxfBytes | ||
| }; | ||
| //#endregion | ||
| export { binaryDxfToText, dxfParser, isBinaryDxf, parseDxf, parseDxfBytes, sniffDxf }; |
| //#region src/parse/errors.d.ts | ||
| /** | ||
| * The one parse-failure type, shared by every format (PARSE-12, PARSE-13). | ||
| * | ||
| * Messages are phrased for a person and shown directly by callers on every | ||
| * surface (viewer, API, MCP); a library's own internals never reach here. | ||
| * `format` names the parser that claimed a file and then rejected it, so a | ||
| * caller can report the culprit without matching on message text — it is | ||
| * undefined when no parser claimed the input at all. | ||
| */ | ||
| declare class DrawingParseError extends Error { | ||
| readonly format?: string; | ||
| constructor(message: string, format?: string); | ||
| } | ||
| //#endregion | ||
| export { DrawingParseError as t }; |
+207
| import { d as DrawingDocument, t as DrawingParser } from "./registry-CtIhdVSA.mjs"; | ||
| //#region src/parse/pdf/objects.d.ts | ||
| /** | ||
| * The PDF object model and its lexer (PDF-2). | ||
| * | ||
| * PDF's syntax is shared by two very different things: the file's object | ||
| * graph, and the content streams that draw. One lexer serves both — content | ||
| * streams are just operands followed by an operator keyword — so this module | ||
| * stays free of any notion of pages or drawing. | ||
| */ | ||
| /** A `/Name`. Wrapped so it never collides with a string operand. */ | ||
| interface PdfName { | ||
| readonly name: string; | ||
| } | ||
| /** An indirect reference, `12 0 R`. */ | ||
| interface PdfRef { | ||
| readonly num: number; | ||
| readonly gen: number; | ||
| } | ||
| /** A PDF string: bytes, not text — the encoding depends on where it is used. */ | ||
| interface PdfString { | ||
| readonly bytes: Uint8Array; | ||
| } | ||
| /** A bare keyword: `obj`, `stream`, or a content-stream operator like `re`. */ | ||
| interface PdfKeyword { | ||
| readonly op: string; | ||
| } | ||
| type PdfDict = Map<string, PdfValue>; | ||
| type PdfValue = number | boolean | null | PdfName | PdfRef | PdfString | PdfKeyword | PdfDict | PdfValue[]; | ||
| //#endregion | ||
| //#region src/parse/pdf/filters.d.ts | ||
| /** | ||
| * A stream this build did not decode — an image codec, or data too damaged to | ||
| * inflate. | ||
| * | ||
| * Decoding never throws. Every caller already branches on this type for image | ||
| * codecs, so making damage take the same path means a caller cannot forget to | ||
| * handle it: a stream-level failure can no longer become a page- or | ||
| * document-level one by omission. | ||
| */ | ||
| interface UndecodedStream { | ||
| /** The filter that stopped us, e.g. "DCTDecode". */ | ||
| readonly unsupportedFilter: string; | ||
| /** True when the filter is one we support but the data would not decode. */ | ||
| readonly damaged?: boolean; | ||
| } | ||
| //#endregion | ||
| //#region src/parse/pdf/document.d.ts | ||
| /** A stream object: its dictionary plus the still-encoded bytes. */ | ||
| interface PdfStream { | ||
| readonly dict: PdfDict; | ||
| readonly raw: Uint8Array; | ||
| } | ||
| declare class PdfDocument { | ||
| private readonly bytes; | ||
| private readonly xref; | ||
| private readonly trailers; | ||
| private readonly objects; | ||
| private readonly objectStreams; | ||
| private readonly decoded; | ||
| private truncated; | ||
| private constructor(); | ||
| static parse(bytes: Uint8Array): Promise<PdfDocument>; | ||
| /** | ||
| * How many streams decoded only partially — a drawing built from them may be | ||
| * missing content, which PDF-8 requires reporting rather than hiding. | ||
| */ | ||
| get truncatedStreams(): number; | ||
| /** Trailer entries, newest section first. */ | ||
| trailerValue(key: string): PdfValue | undefined; | ||
| /** Resolve one level of indirection. */ | ||
| resolve(value: PdfValue | undefined): Promise<PdfValue | undefined>; | ||
| /** Resolve to a dictionary, or undefined when the value is anything else. */ | ||
| dict(value: PdfValue | undefined): Promise<PdfDict | undefined>; | ||
| /** Resolve to an array; a lone value becomes a one-element array. */ | ||
| array(value: PdfValue | undefined): Promise<PdfValue[]>; | ||
| getObject(num: number): Promise<PdfValue | PdfStream | undefined>; | ||
| /** Decode a stream's bytes, or report the image codec that stopped us. */ | ||
| readStream(stream: PdfStream): Promise<Uint8Array | UndecodedStream>; | ||
| /** | ||
| * The stream objects backing a page's content, before decoding. | ||
| * | ||
| * Callers that need to inspect the stream dictionaries — rather than the | ||
| * bytes — use this; `pageContent` is the decoded form. | ||
| */ | ||
| contentStreams(page: PdfDict): Promise<PdfStream[]>; | ||
| /** | ||
| * Every page, in document order. | ||
| * | ||
| * Inheritable attributes (`/Resources`, `/MediaBox`, `/Rotate`) are folded | ||
| * down from ancestors, because a page that omits them means "use my | ||
| * parent's" — not "I have none". | ||
| */ | ||
| /** The document catalog — the trailer's `/Root`. */ | ||
| catalog(): Promise<PdfDict | undefined>; | ||
| pages(): Promise<PdfDict[]>; | ||
| private collectPages; | ||
| /** | ||
| * A page's content, decoded and concatenated. | ||
| * | ||
| * `/Contents` is a stream, an array of streams, or a reference to either — | ||
| * page 1 of the Ghent X-4 suite is an eight-part array. The parts join with | ||
| * a newline because a lexical token may not span the join (PDF 32000-1 | ||
| * §7.7.3.3): without a separator, a trailing `0` and a leading `0` would | ||
| * read as a single `00`. | ||
| */ | ||
| pageContent(page: PdfDict): Promise<Uint8Array>; | ||
| private readXrefChain; | ||
| private findStartXref; | ||
| /** Read one section; returns its `/Prev` offset when the chain continues. */ | ||
| private readXrefSection; | ||
| private readXrefTable; | ||
| private readXrefStream; | ||
| /** | ||
| * Last resort: index every `N G obj` header in the file. | ||
| * | ||
| * Scans bytes rather than decoding the file into a string — recovery is | ||
| * triggered by damage, and a damaged multi-megabyte upload is exactly where | ||
| * materializing a second copy of the file would hurt. | ||
| */ | ||
| private recoverByScan; | ||
| private readObjectAt; | ||
| /** | ||
| * Slice a stream's bytes, starting after the `stream` keyword. | ||
| * | ||
| * `/Length` may be an indirect reference, and some producers write it | ||
| * wrong, so the declared end is verified against the `endstream` keyword | ||
| * and re-derived by search when it doesn't line up. | ||
| */ | ||
| private readStreamBytes; | ||
| private endstreamFollows; | ||
| private readFromObjectStream; | ||
| /** | ||
| * Resolve a reference that must already be readable without inflating — | ||
| * `/Length` and filter parameters, which never live in object streams. | ||
| */ | ||
| private resolveSync; | ||
| } | ||
| //#endregion | ||
| //#region src/parse/pdf/optional-content.d.ts | ||
| /** One optional-content group, in panel order. */ | ||
| interface OcgLayer { | ||
| /** Object-number key; layer identity is document-wide (PDF-7). */ | ||
| readonly key: string; | ||
| readonly name: string; | ||
| readonly visible: boolean; | ||
| } | ||
| /** What an `/OC` reference means for the content it marks. */ | ||
| interface OcResolution { | ||
| /** Layer to place content on; absent leaves it on "Content". */ | ||
| readonly layerKey?: string; | ||
| /** PDF-8 kind to count, when resolving simplified or refused. */ | ||
| readonly counted?: string; | ||
| } | ||
| declare class OptionalContent { | ||
| /** Groups in panel order: `/Order` first, then the rest of `/OCGs`. */ | ||
| readonly layers: readonly OcgLayer[]; | ||
| private readonly doc; | ||
| private readonly names; | ||
| /** Membership dictionaries resolve once and are referenced many times over — | ||
| * one corpus file has 49 of them behind 1041 references. */ | ||
| private readonly cache; | ||
| constructor(layers: readonly OcgLayer[], names?: ReadonlyMap<string, string>, doc?: PdfDocument); | ||
| /** Empty model — a file with no `/OCProperties` at all. */ | ||
| static empty(): OptionalContent; | ||
| get isEmpty(): boolean; | ||
| /** | ||
| * Resolve an `/OC` value (from `BDC` properties or an XObject's `/OC`). | ||
| * | ||
| * Resolves on demand rather than by scanning every object: membership | ||
| * dictionaries are not listed in `/OCProperties`, and only the ones content | ||
| * actually references matter. Returns undefined when the value names nothing | ||
| * we know — unmarked content, which stays on "Content" counting nothing. | ||
| */ | ||
| resolve(value: PdfValue | undefined): Promise<OcResolution | undefined>; | ||
| private resolveMembership; | ||
| /** Display name for a layer key. */ | ||
| nameOf(key: string): string | undefined; | ||
| } | ||
| //#endregion | ||
| //#region src/parse/pdf/interpret.d.ts | ||
| interface InterpretOptions { | ||
| /** Segments per full circle when flattening curves. */ | ||
| curveSegments?: number; | ||
| /** Optional-content model; absent leaves everything on "Content" (PDF-7). */ | ||
| optionalContent?: OptionalContent; | ||
| } | ||
| //#endregion | ||
| //#region src/parse/pdf/parse.d.ts | ||
| interface ParsePdfOptions extends InterpretOptions {} | ||
| /** | ||
| * Parse PDF bytes into a drawing document. | ||
| * | ||
| * Page 1 becomes model space and later pages become named spaces, because the | ||
| * viewer opens model space on load — a PDF whose first page lived in a layout | ||
| * would open blank. | ||
| */ | ||
| declare function parsePdfBytes(source: Uint8Array, options?: ParsePdfOptions): Promise<DrawingDocument>; | ||
| /** True when the bytes start with a PDF header (PARSE-13's sniff). */ | ||
| declare function sniffPdf(bytes: Uint8Array): boolean; | ||
| //#endregion | ||
| //#region src/pdf.d.ts | ||
| /** The PDF parser, ready to pass to `parsers` (VIEW-15) or `parseWith`. */ | ||
| declare const pdfParser: DrawingParser; | ||
| //#endregion | ||
| export { type ParsePdfOptions, parsePdfBytes, pdfParser, sniffPdf }; |
Sorry, the diff of this file is too big to display
| //#region src/model/types.d.ts | ||
| /** Normalized document model. Decoupled from the parser's output shape. */ | ||
| interface Point2 { | ||
| x: number; | ||
| y: number; | ||
| } | ||
| interface Point3 { | ||
| x: number; | ||
| y: number; | ||
| z: number; | ||
| } | ||
| /** 2D affine transform: [a, b, c, d, tx, ty] mapping (x,y) → (a·x+c·y+tx, b·x+d·y+ty). */ | ||
| type Affine2D = [number, number, number, number, number, number]; | ||
| interface Bounds { | ||
| minX: number; | ||
| minY: number; | ||
| maxX: number; | ||
| maxY: number; | ||
| } | ||
| interface LayerInfo { | ||
| name: string; | ||
| /** Layer-table color, 24-bit RGB. May differ from what is drawn. */ | ||
| color: number; | ||
| /** | ||
| * Colors actually drawn on this layer, dominant first (populated after | ||
| * tessellation). Entity-styled files override the table color per entity, | ||
| * so UI should prefer `effectiveColors[0]` over `color`. | ||
| */ | ||
| effectiveColors?: number[]; | ||
| visible: boolean; | ||
| frozen: boolean; | ||
| /** Number of top-level entities on this layer. */ | ||
| entityCount: number; | ||
| /** Layer's default linetype name (resolved against the document map). */ | ||
| lineType?: string; | ||
| /** | ||
| * Layer's default lineweight in 1/100 mm (DXF group 370). Negative codes | ||
| * (-3 default, -2 ByBlock, -1 ByLayer) are dropped to `undefined`. | ||
| */ | ||
| lineWeight?: number; | ||
| } | ||
| interface EntityBase { | ||
| layer: string; | ||
| /** Resolved 24-bit RGB, or null for ByLayer/ByBlock. */ | ||
| color: number | null; | ||
| /** | ||
| * OCS extrusion normal (codes 210/220/230) for entity types whose | ||
| * coordinates are OCS-relative (ARC, POLYLINE, INSERT). Undefined means | ||
| * the default +Z (world coordinates). (0,0,-1) marks mirrored entities. | ||
| */ | ||
| extrusion?: Point3; | ||
| /** | ||
| * Linetype name, or "BYLAYER"/undefined to inherit the layer's. Resolved | ||
| * against the document's `lineTypes` map to a dash pattern at render time. | ||
| */ | ||
| lineType?: string; | ||
| /** | ||
| * Lineweight in 1/100 mm (DXF group 370), or undefined to inherit the | ||
| * layer's. Negative "ByLayer/ByBlock/default" codes are dropped to | ||
| * undefined so the layer default applies. | ||
| */ | ||
| lineWeight?: number; | ||
| } | ||
| /** | ||
| * Linetype dash pattern: alternating drawn/gap lengths in drawing units. | ||
| * Positive = dash (pen down), negative = gap (pen up), 0 = dot. | ||
| */ | ||
| interface LineTypeDef { | ||
| name: string; | ||
| pattern: number[]; | ||
| /** Sum of |pattern|; 0 for a continuous line. */ | ||
| patternLength: number; | ||
| } | ||
| interface LineEntity extends EntityBase { | ||
| type: "LINE"; | ||
| start: Point2; | ||
| end: Point2; | ||
| } | ||
| interface PolylineEntity extends EntityBase { | ||
| type: "POLYLINE"; | ||
| points: Point2[]; | ||
| /** Bulge per segment starting at points[i]; same length as points. */ | ||
| bulges: number[]; | ||
| closed: boolean; | ||
| } | ||
| interface CircleEntity extends EntityBase { | ||
| type: "CIRCLE"; | ||
| center: Point2; | ||
| radius: number; | ||
| } | ||
| interface ArcEntity extends EntityBase { | ||
| type: "ARC"; | ||
| center: Point2; | ||
| radius: number; | ||
| /** Radians, CCW from +X. */ | ||
| startAngle: number; | ||
| endAngle: number; | ||
| } | ||
| interface EllipseEntity extends EntityBase { | ||
| type: "ELLIPSE"; | ||
| center: Point2; | ||
| /** Major axis endpoint relative to center. */ | ||
| majorAxis: Point2; | ||
| /** Minor/major ratio. */ | ||
| axisRatio: number; | ||
| /** Parametric range in radians. */ | ||
| startParam: number; | ||
| endParam: number; | ||
| } | ||
| interface InsertEntity extends EntityBase { | ||
| type: "INSERT"; | ||
| blockName: string; | ||
| position: Point2; | ||
| scale: Point2; | ||
| /** Radians. */ | ||
| rotation: number; | ||
| } | ||
| type TextHAlign = "left" | "center" | "right"; | ||
| type TextVAlign = "baseline" | "bottom" | "middle" | "top"; | ||
| /** Normalized TEXT and MTEXT. MTEXT format codes are collapsed to plain text. */ | ||
| interface TextEntity extends EntityBase { | ||
| type: "TEXT"; | ||
| /** Insertion/alignment point. */ | ||
| position: Point2; | ||
| /** Content; may contain newlines (from MTEXT paragraphs). */ | ||
| text: string; | ||
| /** Cap height in drawing units. */ | ||
| height: number; | ||
| /** Radians, CCW. */ | ||
| rotation: number; | ||
| /** Horizontal scale (DXF xScale). */ | ||
| widthFactor: number; | ||
| hAlign: TextHAlign; | ||
| vAlign: TextVAlign; | ||
| } | ||
| interface SplineEntity extends EntityBase { | ||
| type: "SPLINE"; | ||
| controlPoints: Point2[]; | ||
| /** Knot vector; empty means "generate a clamped uniform vector". */ | ||
| knots: number[]; | ||
| degree: number; | ||
| closed: boolean; | ||
| } | ||
| /** Filled triangle/quad: SOLID, TRACE, or a projected 3DFACE. */ | ||
| interface SolidEntity extends EntityBase { | ||
| type: "SOLID"; | ||
| /** 3 or 4 corners, already reordered to a simple (non-crossing) ring. */ | ||
| points: Point2[]; | ||
| } | ||
| /** A POINT — rendered as a small crosshair marker. */ | ||
| interface PointEntity extends EntityBase { | ||
| type: "POINT"; | ||
| position: Point2; | ||
| } | ||
| /** DIMENSION — rendered by drawing its anonymous geometry block. */ | ||
| interface DimensionEntity extends EntityBase { | ||
| type: "DIMENSION"; | ||
| /** Name of the anonymous "*D…" block holding the lines, arrows, and text. */ | ||
| block: string; | ||
| /** Block insertion point (usually the origin). */ | ||
| position: Point2; | ||
| } | ||
| /** HATCH — filled region(s). Boundaries are pre-sampled to polyline loops. */ | ||
| interface HatchEntity extends EntityBase { | ||
| type: "HATCH"; | ||
| /** Boundary loops in drawing coordinates (outer + holes, unspecified order). */ | ||
| loops: Point2[][]; | ||
| /** Solid fill vs. a line pattern (patterns render as boundary outlines). */ | ||
| solid: boolean; | ||
| } | ||
| type Entity = LineEntity | PolylineEntity | CircleEntity | ArcEntity | EllipseEntity | InsertEntity | TextEntity | SplineEntity | SolidEntity | PointEntity | DimensionEntity | HatchEntity; | ||
| type EntityType = Entity["type"]; | ||
| interface BlockDef { | ||
| name: string; | ||
| basePoint: Point2; | ||
| entities: Entity[]; | ||
| } | ||
| /** A paper-space viewport: a window that frames model space at a fixed scale. */ | ||
| interface Viewport { | ||
| /** Center of the window on the paper (paper coords). */ | ||
| center: Point2; | ||
| /** Window size on the paper. */ | ||
| width: number; | ||
| height: number; | ||
| /** Model point shown at the window center. */ | ||
| viewCenter: Point2; | ||
| /** Model-space height visible through the window (drives the scale). */ | ||
| viewHeight: number; | ||
| /** View twist, radians (CCW). */ | ||
| twist: number; | ||
| } | ||
| /** A paper-space layout: a printable sheet with its own geometry and viewports. */ | ||
| interface Layout { | ||
| name: string; | ||
| /** Drawable paper-space geometry (titleblock, borders, text). */ | ||
| entities: Entity[]; | ||
| /** Windows into model space. */ | ||
| viewports: Viewport[]; | ||
| } | ||
| interface DrawingDocument { | ||
| layers: Map<string, LayerInfo>; | ||
| entities: Entity[]; | ||
| blocks: Map<string, BlockDef>; | ||
| /** Linetype definitions from the LTYPE table, keyed by name. */ | ||
| lineTypes: Map<string, LineTypeDef>; | ||
| /** Counts of raw DXF entity types that were skipped by the parser stage. */ | ||
| unsupported: Record<string, number>; | ||
| /** | ||
| * Short drawing-unit label from the header's `$INSUNITS` (e.g. "mm", "in"), | ||
| * or "" when the drawing is unitless or the code is unknown. `parseDxf` | ||
| * always sets it; hand-built documents may omit it (treated as ""). | ||
| */ | ||
| units?: string; | ||
| /** | ||
| * Paper-space layouts, if any. `entities` holds model space; each layout | ||
| * carries its own paper geometry and viewports. `parseDxf` sets it (possibly | ||
| * empty); hand-built documents may omit it (treated as no layouts). | ||
| */ | ||
| layouts?: Layout[]; | ||
| /** | ||
| * Which format produced this document ("dxf", "pdf") — so every surface can | ||
| * report it without re-sniffing the bytes (PARSE-13). Parsers always set it; | ||
| * hand-built documents may omit it. | ||
| */ | ||
| format?: string; | ||
| } | ||
| //#endregion | ||
| //#region src/parse/registry.d.ts | ||
| /** One file format's contribution: a name, a byte sniff, and a parse. */ | ||
| interface DrawingParser { | ||
| /** Short lowercase format name, e.g. "dxf". Surfaces report it verbatim. */ | ||
| format: string; | ||
| /** | ||
| * True when this parser claims the bytes. Sniffs see the whole buffer but | ||
| * should only look at the head — they run on every load, for every parser. | ||
| */ | ||
| sniff(bytes: Uint8Array): boolean; | ||
| /** Parse claimed bytes, or throw a `DrawingParseError` carrying `format`. */ | ||
| parse(bytes: Uint8Array): DrawingDocument | Promise<DrawingDocument>; | ||
| } | ||
| /** Everything a drawing can be loaded from (PARSE-1). */ | ||
| type DrawingSource = string | ArrayBuffer | Uint8Array | Blob; | ||
| /** Normalize any accepted source to bytes, so sniffs see one shape (PARSE-1). */ | ||
| declare function toBytes(source: DrawingSource): Promise<Uint8Array>; | ||
| /** | ||
| * Parse `source` with the first parser whose sniff claims it (PARSE-13). | ||
| * | ||
| * Sniffs run in the order given, so a caller controls precedence by ordering | ||
| * its list. No parser claiming the bytes is a clean, honest failure, not a | ||
| * fallback attempt at every parser in turn (PARSE-12). | ||
| */ | ||
| declare function parseWith(parsers: readonly DrawingParser[], source: DrawingSource): Promise<DrawingDocument>; | ||
| //#endregion | ||
| export { Viewport as A, PointEntity as C, TextEntity as D, SplineEntity as E, TextHAlign as O, Point3 as S, SolidEntity as T, LayerInfo as _, Affine2D as a, LineTypeDef as b, Bounds as c, DrawingDocument as d, EllipseEntity as f, InsertEntity as g, HatchEntity as h, toBytes as i, TextVAlign as k, CircleEntity as l, EntityType as m, DrawingSource as n, ArcEntity as o, Entity as p, parseWith as r, BlockDef as s, DrawingParser as t, DimensionEntity as u, Layout as v, PolylineEntity as w, Point2 as x, LineEntity as y }; |
| //#region src/text/font.ts | ||
| /** | ||
| * Single-stroke vector font for rendering DXF text as polylines — the same | ||
| * approach CAD uses (SHX stroke fonts), so text flows through the existing | ||
| * line-batching renderer with no glyph triangulation or webfonts. | ||
| * | ||
| * Data: the public-domain Hershey "futural" (Simplex Roman) set, ASCII 32-126, | ||
| * base64-encoded to survive the backslash/backtick coordinate characters. | ||
| * Format per line: cols 0-4 ignored, cols 5-7 = vertex count, then coordinate | ||
| * pairs (char - 'R'); a leading space starts a new stroke (pen up); the first | ||
| * pair is the left/right spacing bounds. | ||
| * Source: github.com/kamalmostafa/hershey-fonts (futural.jhf). | ||
| */ | ||
| const FONT_B64 = "MTIzNDUgIDFKWgoxMjM0NSAgOU1XUkZSVCBSUllRWlJbU1pSWQoxMjM0NSAgNkpaTkZOTSBSVkZWTQoxMjM0NSAxMkhdU0JMYiBSWUJSYiBSTE9aTyBSS1VZVQoxMjM0NSAyN0hcUEJQXyBSVEJUXyBSWUlXR1RGUEZNR0tJS0tMTU1OT09VUVdSWFNZVVlYV1pUW1BbTVpLWAoxMjM0NSAzMkZeW0ZJWyBSTkZQSFBKT0xNTUtNSUtJSUpHTEZORlBHU0hWSFlHW0YgUldUVVVUV1RZVltYW1paW1hbVllUV1QKMTIzNDUgMzVFX1xPXE5bTVpNWU5YUFZVVFhSWlBbTFtKWklZSFdIVUlTSlJRTlJNU0tTSVJHUEZOR01JTUtOTlBRVVhXWllbW1tcWlxZCjEyMzQ1ICA4TVdSSFFHUkZTR1NJUktRTAoxMjM0NSAxMUtZVkJURFJHUEtPUE9UUFlSXVRgVmIKMTIzNDUgMTFLWU5CUERSR1RLVVBVVFRZUl1QYE5iCjEyMzQ1ICA5SlpSTFJYIFJNT1dVIFJXT01VCjEyMzQ1ICA2RV9SSVJbIFJJUltSCjEyMzQ1ICA4TlZTV1JYUVdSVlNXU1lRWwoxMjM0NSAgM0VfSVJbUgoxMjM0NSAgNk5WUlZRV1JYU1dSVgoxMjM0NSAgM0ddW0JJYgoxMjM0NSAxOEhcUUZOR0xKS09LUkxXTlpRW1NbVlpYV1lSWU9YSlZHU0ZRRgoxMjM0NSAgNUhcTkpQSVNGU1sKMTIzNDUgMTVIXExLTEpNSE5HUEZURlZHV0hYSlhMV05VUUtbWVsKMTIzNDUgMTZIXE1GWEZSTlVOV09YUFlTWVVYWFZaU1tQW01aTFlLVwoxMjM0NSAgN0hcVUZLVFpUIFJVRlVbCjEyMzQ1IDE4SFxXRk1GTE9NTlBNU01WTlhQWVNZVVhYVlpTW1BbTVpMWUtXCjEyMzQ1IDI0SFxYSVdHVEZSRk9HTUpMT0xUTVhPWlJbU1tWWlhYWVVZVFhRVk9TTlJOT09NUUxUCjEyMzQ1ICA2SFxZRk9bIFJLRllGCjEyMzQ1IDMwSFxQRk1HTElMS01NT05TT1ZQWFJZVFlXWFlXWlRbUFtNWkxZS1dLVExSTlBRT1VOV01YS1hJV0dURlBGCjEyMzQ1IDI0SFxYTVdQVVJSU1FTTlJMUEtNS0xMSU5HUUZSRlVHV0lYTVhSV1dVWlJbUFtNWkxYCjEyMzQ1IDEyTlZST1FQUlFTUFJPIFJSVlFXUlhTV1JWCjEyMzQ1IDE0TlZST1FQUlFTUFJPIFJTV1JYUVdSVlNXU1lRWwoxMjM0NSAgNEZeWklKUlpbCjEyMzQ1ICA2RV9JT1tPIFJJVVtVCjEyMzQ1ICA0Rl5KSVpSSlsKMTIzNDUgMjFJW0xLTEpNSE5HUEZURlZHV0hYSlhMV05WT1JRUlQgUlJZUVpSW1NaUlkKMTIzNDUgNTZFYFdOVkxUS1FLT0xOTU1QTVNOVVBWU1ZVVVZTIFJRS09NTlBOU09VUFYgUldLVlNWVVhWWlZcVF1RXU9cTFtKWUhXR1RGUUZOR0xISkpJTEhPSFJJVUpXTFlOWlFbVFtXWllZWlggUlhLV1NXVVhWCjEyMzQ1ICA5SVtSRkpbIFJSRlpbIFJNVFdUCjEyMzQ1IDI0R1xLRktbIFJLRlRGV0dYSFlKWUxYTldPVFAgUktQVFBXUVhSWVRZV1hZV1pUW0tbCjEyMzQ1IDE5SF1aS1lJV0dVRlFGT0dNSUxLS05LU0xWTVhPWlFbVVtXWllYWlYKMTIzNDUgMTZHXEtGS1sgUktGUkZVR1dJWEtZTllTWFZXWFVaUltLWwoxMjM0NSAxMkhbTEZMWyBSTEZZRiBSTFBUUCBSTFtZWwoxMjM0NSAgOUhaTEZMWyBSTEZZRiBSTFBUUAoxMjM0NSAyM0hdWktZSVdHVUZRRk9HTUlMS0tOS1NMVk1YT1pRW1VbV1pZWFpWWlMgUlVTWlMKMTIzNDUgIDlHXUtGS1sgUllGWVsgUktQWVAKMTIzNDUgIDNOVlJGUlsKMTIzNDUgMTFKWlZGVlZVWVRaUltQW05aTVlMVkxUCjEyMzQ1ICA5R1xLRktbIFJZRktUIFJQT1lbCjEyMzQ1ICA2SFlMRkxbIFJMW1hbCjEyMzQ1IDEyRl5KRkpbIFJKRlJbIFJaRlJbIFJaRlpbCjEyMzQ1ICA5R11LRktbIFJLRllbIFJZRllbCjEyMzQ1IDIyR11QRk5HTElLS0pOSlNLVkxYTlpQW1RbVlpYWFlWWlNaTllLWElWR1RGUEYKMTIzNDUgMTRHXEtGS1sgUktGVEZXR1hIWUpZTVhPV1BUUUtRCjEyMzQ1IDI1R11QRk5HTElLS0pOSlNLVkxYTlpQW1RbVlpYWFlWWlNaTllLWElWR1RGUEYgUlNXWV0KMTIzNDUgMTdHXEtGS1sgUktGVEZXR1hIWUpZTFhOV09UUEtQIFJSUFlbCjEyMzQ1IDIxSFxZSVdHVEZQRk1HS0lLS0xNTU5PT1VRV1JYU1lVWVhXWlRbUFtNWktYCjEyMzQ1ICA2SlpSRlJbIFJLRllGCjEyMzQ1IDExR11LRktVTFhOWlFbU1tWWlhYWVVZRgoxMjM0NSAgNklbSkZSWyBSWkZSWwoxMjM0NSAxMkZeSEZNWyBSUkZNWyBSUkZXWyBSXEZXWwoxMjM0NSAgNkhcS0ZZWyBSWUZLWwoxMjM0NSAgN0lbSkZSUFJbIFJaRlJQCjEyMzQ1ICA5SFxZRktbIFJLRllGIFJLW1lbCjEyMzQ1IDEyS1lPQk9iIFJQQlBiIFJPQlZCIFJPYlZiCjEyMzQ1ICAzS1lLRlleCjEyMzQ1IDEyS1lUQlRiIFJVQlViIFJOQlVCIFJOYlViCjEyMzQ1ICA2SlpSREpSIFJSRFpSCjEyMzQ1ICAzSVtJYltiCjEyMzQ1ICA4TlZTS1FNUU9SUFNPUk5RTwoxMjM0NSAxOElcWE1YWyBSWFBWTlRNUU1PTk1QTFNMVU1YT1pRW1RbVlpYWAoxMjM0NSAxOEhbTEZMWyBSTFBOTlBNU01VTldQWFNYVVdYVVpTW1BbTlpMWAoxMjM0NSAxNUlbWFBWTlRNUU1PTk1QTFNMVU1YT1pRW1RbVlpYWAoxMjM0NSAxOElcWEZYWyBSWFBWTlRNUU1PTk1QTFNMVU1YT1pRW1RbVlpYWAoxMjM0NSAxOElbTFNYU1hRV09WTlRNUU1PTk1QTFNMVU1YT1pRW1RbVlpYWAoxMjM0NSAgOU1ZV0ZVRlNHUkpSWyBST01WTQoxMjM0NSAyM0lcWE1YXVdgVmFUYlFiT2EgUlhQVk5UTVFNT05NUExTTFVNWE9aUVtUW1ZaWFgKMTIzNDUgMTFJXE1GTVsgUk1RUE5STVVNV05YUVhbCjEyMzQ1ICA5TlZRRlJHU0ZSRVFGIFJSTVJbCjEyMzQ1IDEyTVdSRlNHVEZTRVJGIFJTTVNeUmFQYk5iCjEyMzQ1ICA5SVpNRk1bIFJXTU1XIFJRU1hbCjEyMzQ1ICAzTlZSRlJbCjEyMzQ1IDE5Q2FHTUdbIFJHUUpOTE1PTVFOUlFSWyBSUlFVTldNWk1cTl1RXVsKMTIzNDUgMTFJXE1NTVsgUk1RUE5STVVNV05YUVhbCjEyMzQ1IDE4SVxRTU9OTVBMU0xVTVhPWlFbVFtWWlhYWVVZU1hQVk5UTVFNCjEyMzQ1IDE4SFtMTUxiIFJMUE5OUE1TTVVOV1BYU1hVV1hVWlNbUFtOWkxYCjEyMzQ1IDE4SVxYTVhiIFJYUFZOVE1RTU9OTVBMU0xVTVhPWlFbVFtWWlhYCjEyMzQ1ICA5S1hPTU9bIFJPU1BQUk5UTVdNCjEyMzQ1IDE4SltYUFdOVE1RTU5OTVBOUlBTVVRXVVhXWFhXWlRbUVtOWk1YCjEyMzQ1ICA5TVlSRlJXU1pVW1dbIFJPTVZNCjEyMzQ1IDExSVxNTU1XTlpQW1NbVVpYVyBSWE1YWwoxMjM0NSAgNkpaTE1SWyBSWE1SWwoxMjM0NSAxMkddSk1OWyBSUk1OWyBSUk1WWyBSWk1WWwoxMjM0NSAgNkpbTU1YWyBSWE1NWwoxMjM0NSAxMEpaTE1SWyBSWE1SW1BfTmFMYktiCjEyMzQ1ICA5SltYTU1bIFJNTVhNIFJNW1hbCjEyMzQ1IDQwS1lUQlJDUURQRlBIUUpSS1NNU09RUSBSUkNRRVFHUklTSlRMVE5TUE9SU1RUVlRYU1pSW1FdUV9SYSBSUVNTVVNXUllRWlBcUF5RYFJhVGIKMTIzNDUgIDNOVlJCUmIKMTIzNDUgNDBLWVBCUkNTRFRGVEhTSlJLUU1RT1NRIFJSQ1NFU0dSSVFKUExQTlFQVVJRVFBWUFhRWlJbU11TX1JhIFJTU1FVUVdSWVNaVFxUXlNgUmFQYgoxMjM0NSAyNEZeSVVJU0pQTE9OT1BQVFNWVFhUWlNbUSBSSVNKUUxQTlBQUVRUVlVYVVpUW1FbTwoxMjM0NSAzNUpaSkZKW0tbS0ZMRkxbTVtNRk5GTltPW09GUEZQW1FbUUZSRlJbU1tTRlRGVFtVW1VGVkZWW1dbV0ZYRlhbWVtZRlpGWlsK"; | ||
| const decodeBase64 = (b64) => typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("binary"); | ||
| let lines = null; | ||
| const cache = /* @__PURE__ */ new Map(); | ||
| function fontLines() { | ||
| if (!lines) lines = decodeBase64(FONT_B64).split("\n").filter((l) => l.length > 0); | ||
| return lines; | ||
| } | ||
| function decodeGlyph(line) { | ||
| const nvert = parseInt(line.slice(5, 8), 10); | ||
| const data = line.slice(8); | ||
| const left = data.charCodeAt(0) - 82; | ||
| const right = data.charCodeAt(1) - 82; | ||
| const strokes = []; | ||
| let current = []; | ||
| for (let i = 1; i < nvert; i++) if (data[2 * i] === " ") { | ||
| strokes.push(current); | ||
| current = []; | ||
| } else current.push({ | ||
| x: data.charCodeAt(2 * i) - 82, | ||
| y: data.charCodeAt(2 * i + 1) - 82 | ||
| }); | ||
| strokes.push(current); | ||
| return { | ||
| strokes: strokes.filter((s) => s.length > 0), | ||
| advance: right - left, | ||
| left | ||
| }; | ||
| } | ||
| /** Sample a full circle as a closed polyline (font units, y down). */ | ||
| function circleStroke(cx, cy, r, segments = 12) { | ||
| const pts = []; | ||
| for (let i = 0; i <= segments; i++) { | ||
| const a = i / segments * 2 * Math.PI; | ||
| pts.push({ | ||
| x: cx + r * Math.cos(a), | ||
| y: cy + r * Math.sin(a) | ||
| }); | ||
| } | ||
| return pts; | ||
| } | ||
| /** | ||
| * Glyphs the Hershey ASCII table lacks but CAD text needs — the %%-code | ||
| * symbols ° ± Ø (PARSE-9). Coordinates follow the font convention: | ||
| * baseline at y=9, cap top at y=-12, y down. | ||
| */ | ||
| const SYNTHETIC = /* @__PURE__ */ new Map([ | ||
| [176, () => ({ | ||
| strokes: [circleStroke(0, -8, 3.5)], | ||
| advance: 11, | ||
| left: -5.5 | ||
| })], | ||
| [177, () => ({ | ||
| strokes: [ | ||
| [{ | ||
| x: 0, | ||
| y: -10 | ||
| }, { | ||
| x: 0, | ||
| y: 2 | ||
| }], | ||
| [{ | ||
| x: -5, | ||
| y: -4 | ||
| }, { | ||
| x: 5, | ||
| y: -4 | ||
| }], | ||
| [{ | ||
| x: -5, | ||
| y: 6 | ||
| }, { | ||
| x: 5, | ||
| y: 6 | ||
| }] | ||
| ], | ||
| advance: 14, | ||
| left: -7 | ||
| })], | ||
| [216, () => { | ||
| const o = glyph(79); | ||
| let minX = Infinity; | ||
| let maxX = -Infinity; | ||
| for (const stroke of o.strokes) for (const p of stroke) { | ||
| minX = Math.min(minX, p.x); | ||
| maxX = Math.max(maxX, p.x); | ||
| } | ||
| const slash = [{ | ||
| x: minX - 1, | ||
| y: 11 | ||
| }, { | ||
| x: maxX + 1, | ||
| y: -14 | ||
| }]; | ||
| return { | ||
| strokes: [...o.strokes, slash], | ||
| advance: o.advance, | ||
| left: o.left | ||
| }; | ||
| }] | ||
| ]); | ||
| /** Glyph for a character code, or the space glyph for unmapped codes. */ | ||
| function glyph(charCode) { | ||
| const index = charCode - 32; | ||
| const all = fontLines(); | ||
| const line = index >= 0 && index < all.length ? all[index] : all[0]; | ||
| let g = cache.get(charCode); | ||
| if (!g) { | ||
| g = SYNTHETIC.get(charCode)?.() ?? decodeGlyph(line); | ||
| cache.set(charCode, g); | ||
| } | ||
| return g; | ||
| } | ||
| //#endregion | ||
| //#region src/text/layout.ts | ||
| /** Baseline is at Hershey y = 9 (bottom of capitals). */ | ||
| const BASELINE = 9; | ||
| const H_FRACTION = { | ||
| left: 0, | ||
| center: .5, | ||
| right: 1 | ||
| }; | ||
| const BSL = String.fromCharCode(1); | ||
| const LBR = String.fromCharCode(2); | ||
| const RBR = String.fromCharCode(3); | ||
| /** Decode DXF \U+XXXX escapes (pre-2007 files store non-ANSI text this way). */ | ||
| const decodeUnicodeEscapes = (s) => s.replace(/\\[Uu]\+([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); | ||
| /** TEXT-era %%-code → character (case-insensitive). */ | ||
| const PERCENT_CHARS = { | ||
| d: "°", | ||
| p: "±", | ||
| c: "Ø" | ||
| }; | ||
| /** | ||
| * Decode legacy TEXT control sequences to plain content (PARSE-9): | ||
| * %%d/%%p/%%c → °/±/Ø, %%u/%%o/%%k style toggles dropped, %%% → %, | ||
| * %%nnn → character nnn, \U+XXXX unescaped, and the caret notation for | ||
| * tab/newline normalized. Unknown %% sequences stay literal. | ||
| */ | ||
| function decodeTextSpecials(raw) { | ||
| let s = raw.replace(/\^([IJM ])/g, (_, ch) => ch === " " ? "^" : String.fromCharCode(ch.charCodeAt(0) - 64)); | ||
| s = decodeUnicodeEscapes(s); | ||
| let out = ""; | ||
| for (let i = 0; i < s.length; i++) { | ||
| if (s[i] === "%" && s[i + 1] === "%" && i + 2 < s.length) { | ||
| const code = s[i + 2].toLowerCase(); | ||
| const special = PERCENT_CHARS[code]; | ||
| if (special) { | ||
| out += special; | ||
| i += 2; | ||
| continue; | ||
| } | ||
| if (code === "u" || code === "o" || code === "k") { | ||
| i += 2; | ||
| continue; | ||
| } | ||
| if (code === "%") { | ||
| out += "%"; | ||
| i += 2; | ||
| continue; | ||
| } | ||
| const nnn = /^\d{3}/.exec(s.slice(i + 2)); | ||
| if (nnn) { | ||
| out += String.fromCharCode(parseInt(nnn[0], 10)); | ||
| i += 4; | ||
| continue; | ||
| } | ||
| } | ||
| out += s[i]; | ||
| } | ||
| return out; | ||
| } | ||
| /** | ||
| * Collapse MTEXT inline formatting codes to plain text. Paragraph breaks | ||
| * become newlines; font/height/color/alignment directives are dropped; | ||
| * stacked fractions are flattened to "a/b". Best-effort — enough to read. | ||
| */ | ||
| function stripMText(raw) { | ||
| let s = raw.replace(/\\\\/g, BSL).replace(/\\\{/g, LBR).replace(/\\\}/g, RBR).replace(/\\~/g, " ").replace(/\\P/g, "\n"); | ||
| s = decodeUnicodeEscapes(s); | ||
| s = s.replace(/\\S([^;^/#]*)[\^/#]([^;]*);/g, "$1/$2"); | ||
| s = s.replace(/\\[A-Za-z][^;\\]*;/g, ""); | ||
| s = s.replace(/\\[LlOoKkNX]/g, ""); | ||
| s = s.replace(/[{}]/g, ""); | ||
| return s.split(BSL).join("\\").split(LBR).join("{").split(RBR).join("}"); | ||
| } | ||
| function lineWidth(line, scale, widthFactor) { | ||
| let advance = 0; | ||
| for (let i = 0; i < line.length; i++) advance += glyph(line.charCodeAt(i)).advance; | ||
| return advance * scale * widthFactor; | ||
| } | ||
| /** | ||
| * Lay out text as stroke polylines around the insertion point (origin), | ||
| * y-up and unrotated. The caller applies the entity's rotation and position. | ||
| */ | ||
| function layoutText(text, options) { | ||
| const { height, widthFactor = 1, hAlign = "left", vAlign = "baseline", lineSpacing = 1.5 } = options; | ||
| const scale = height / 21; | ||
| const lineHeight = height * lineSpacing; | ||
| const lines = text.split("\n"); | ||
| const blockTop = height; | ||
| const blockBottom = -(lines.length - 1) * lineHeight; | ||
| let y0 = 0; | ||
| if (vAlign === "top") y0 = -blockTop; | ||
| else if (vAlign === "bottom") y0 = -blockBottom; | ||
| else if (vAlign === "middle") y0 = -(blockTop + blockBottom) / 2; | ||
| const out = []; | ||
| for (let li = 0; li < lines.length; li++) { | ||
| const line = lines[li]; | ||
| const baseY = y0 - li * lineHeight; | ||
| let penX = -lineWidth(line, scale, widthFactor) * H_FRACTION[hAlign]; | ||
| for (let ci = 0; ci < line.length; ci++) { | ||
| const g = glyph(line.charCodeAt(ci)); | ||
| for (const stroke of g.strokes) { | ||
| const poly = []; | ||
| for (const p of stroke) poly.push({ | ||
| x: penX + (p.x - g.left) * scale * widthFactor, | ||
| y: baseY + (BASELINE - p.y) * scale | ||
| }); | ||
| out.push(poly); | ||
| } | ||
| penX += g.advance * scale * widthFactor; | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| //#endregion | ||
| //#region src/units.ts | ||
| /** | ||
| * DXF drawing units. `$INSUNITS` (header) codes map to short display labels; | ||
| * unknown or unitless drawings return "" so the UI can show bare numbers. | ||
| */ | ||
| const INSUNITS = { | ||
| 0: "", | ||
| 1: "in", | ||
| 2: "ft", | ||
| 3: "mi", | ||
| 4: "mm", | ||
| 5: "cm", | ||
| 6: "m", | ||
| 7: "km", | ||
| 8: "µin", | ||
| 9: "mil", | ||
| 10: "yd", | ||
| 11: "Å", | ||
| 12: "nm", | ||
| 13: "µm", | ||
| 14: "dm", | ||
| 15: "dam", | ||
| 16: "hm", | ||
| 17: "Gm", | ||
| 18: "AU", | ||
| 19: "ly", | ||
| 20: "pc" | ||
| }; | ||
| /** Short unit label for an `$INSUNITS` code (e.g. 4 → "mm"), or "" if unitless/unknown. */ | ||
| function unitLabel(insunits) { | ||
| return insunits !== void 0 && INSUNITS[insunits] || ""; | ||
| } | ||
| /** | ||
| * Largest "nice" length (1, 2, or 5 × 10ⁿ) not exceeding `max`. Used to size a | ||
| * scale bar to a round number of drawing units. Returns 0 for non-positive max. | ||
| */ | ||
| function niceLength(max) { | ||
| if (!(max > 0) || !Number.isFinite(max)) return 0; | ||
| const base = 10 ** Math.floor(Math.log10(max)); | ||
| for (const m of [ | ||
| 5, | ||
| 2, | ||
| 1 | ||
| ]) if (m * base <= max) return m * base; | ||
| return base; | ||
| } | ||
| //#endregion | ||
| export { stripMText as a, layoutText as i, unitLabel as n, decodeTextSpecials as r, niceLength as t }; |
+28
-286
@@ -1,222 +0,4 @@ | ||
| //#region src/model/types.d.ts | ||
| /** Normalized document model. Decoupled from the parser's output shape. */ | ||
| interface Point2 { | ||
| x: number; | ||
| y: number; | ||
| } | ||
| interface Point3 { | ||
| x: number; | ||
| y: number; | ||
| z: number; | ||
| } | ||
| /** 2D affine transform: [a, b, c, d, tx, ty] mapping (x,y) → (a·x+c·y+tx, b·x+d·y+ty). */ | ||
| type Affine2D = [number, number, number, number, number, number]; | ||
| interface Bounds { | ||
| minX: number; | ||
| minY: number; | ||
| maxX: number; | ||
| maxY: number; | ||
| } | ||
| interface LayerInfo { | ||
| name: string; | ||
| /** Layer-table color, 24-bit RGB. May differ from what is drawn. */ | ||
| color: number; | ||
| /** | ||
| * Colors actually drawn on this layer, dominant first (populated after | ||
| * tessellation). Entity-styled files override the table color per entity, | ||
| * so UI should prefer `effectiveColors[0]` over `color`. | ||
| */ | ||
| effectiveColors?: number[]; | ||
| visible: boolean; | ||
| frozen: boolean; | ||
| /** Number of top-level entities on this layer. */ | ||
| entityCount: number; | ||
| /** Layer's default linetype name (resolved against the document map). */ | ||
| lineType?: string; | ||
| /** | ||
| * Layer's default lineweight in 1/100 mm (DXF group 370). Negative codes | ||
| * (-3 default, -2 ByBlock, -1 ByLayer) are dropped to `undefined`. | ||
| */ | ||
| lineWeight?: number; | ||
| } | ||
| interface EntityBase { | ||
| layer: string; | ||
| /** Resolved 24-bit RGB, or null for ByLayer/ByBlock. */ | ||
| color: number | null; | ||
| /** | ||
| * OCS extrusion normal (codes 210/220/230) for entity types whose | ||
| * coordinates are OCS-relative (ARC, POLYLINE, INSERT). Undefined means | ||
| * the default +Z (world coordinates). (0,0,-1) marks mirrored entities. | ||
| */ | ||
| extrusion?: Point3; | ||
| /** | ||
| * Linetype name, or "BYLAYER"/undefined to inherit the layer's. Resolved | ||
| * against the document's `lineTypes` map to a dash pattern at render time. | ||
| */ | ||
| lineType?: string; | ||
| /** | ||
| * Lineweight in 1/100 mm (DXF group 370), or undefined to inherit the | ||
| * layer's. Negative "ByLayer/ByBlock/default" codes are dropped to | ||
| * undefined so the layer default applies. | ||
| */ | ||
| lineWeight?: number; | ||
| } | ||
| /** | ||
| * Linetype dash pattern: alternating drawn/gap lengths in drawing units. | ||
| * Positive = dash (pen down), negative = gap (pen up), 0 = dot. | ||
| */ | ||
| interface LineTypeDef { | ||
| name: string; | ||
| pattern: number[]; | ||
| /** Sum of |pattern|; 0 for a continuous line. */ | ||
| patternLength: number; | ||
| } | ||
| interface LineEntity extends EntityBase { | ||
| type: "LINE"; | ||
| start: Point2; | ||
| end: Point2; | ||
| } | ||
| interface PolylineEntity extends EntityBase { | ||
| type: "POLYLINE"; | ||
| points: Point2[]; | ||
| /** Bulge per segment starting at points[i]; same length as points. */ | ||
| bulges: number[]; | ||
| closed: boolean; | ||
| } | ||
| interface CircleEntity extends EntityBase { | ||
| type: "CIRCLE"; | ||
| center: Point2; | ||
| radius: number; | ||
| } | ||
| interface ArcEntity extends EntityBase { | ||
| type: "ARC"; | ||
| center: Point2; | ||
| radius: number; | ||
| /** Radians, CCW from +X. */ | ||
| startAngle: number; | ||
| endAngle: number; | ||
| } | ||
| interface EllipseEntity extends EntityBase { | ||
| type: "ELLIPSE"; | ||
| center: Point2; | ||
| /** Major axis endpoint relative to center. */ | ||
| majorAxis: Point2; | ||
| /** Minor/major ratio. */ | ||
| axisRatio: number; | ||
| /** Parametric range in radians. */ | ||
| startParam: number; | ||
| endParam: number; | ||
| } | ||
| interface InsertEntity extends EntityBase { | ||
| type: "INSERT"; | ||
| blockName: string; | ||
| position: Point2; | ||
| scale: Point2; | ||
| /** Radians. */ | ||
| rotation: number; | ||
| } | ||
| type TextHAlign = "left" | "center" | "right"; | ||
| type TextVAlign = "baseline" | "bottom" | "middle" | "top"; | ||
| /** Normalized TEXT and MTEXT. MTEXT format codes are collapsed to plain text. */ | ||
| interface TextEntity extends EntityBase { | ||
| type: "TEXT"; | ||
| /** Insertion/alignment point. */ | ||
| position: Point2; | ||
| /** Content; may contain newlines (from MTEXT paragraphs). */ | ||
| text: string; | ||
| /** Cap height in drawing units. */ | ||
| height: number; | ||
| /** Radians, CCW. */ | ||
| rotation: number; | ||
| /** Horizontal scale (DXF xScale). */ | ||
| widthFactor: number; | ||
| hAlign: TextHAlign; | ||
| vAlign: TextVAlign; | ||
| } | ||
| interface SplineEntity extends EntityBase { | ||
| type: "SPLINE"; | ||
| controlPoints: Point2[]; | ||
| /** Knot vector; empty means "generate a clamped uniform vector". */ | ||
| knots: number[]; | ||
| degree: number; | ||
| closed: boolean; | ||
| } | ||
| /** Filled triangle/quad: SOLID, TRACE, or a projected 3DFACE. */ | ||
| interface SolidEntity extends EntityBase { | ||
| type: "SOLID"; | ||
| /** 3 or 4 corners, already reordered to a simple (non-crossing) ring. */ | ||
| points: Point2[]; | ||
| } | ||
| /** A POINT — rendered as a small crosshair marker. */ | ||
| interface PointEntity extends EntityBase { | ||
| type: "POINT"; | ||
| position: Point2; | ||
| } | ||
| /** DIMENSION — rendered by drawing its anonymous geometry block. */ | ||
| interface DimensionEntity extends EntityBase { | ||
| type: "DIMENSION"; | ||
| /** Name of the anonymous "*D…" block holding the lines, arrows, and text. */ | ||
| block: string; | ||
| /** Block insertion point (usually the origin). */ | ||
| position: Point2; | ||
| } | ||
| /** HATCH — filled region(s). Boundaries are pre-sampled to polyline loops. */ | ||
| interface HatchEntity extends EntityBase { | ||
| type: "HATCH"; | ||
| /** Boundary loops in drawing coordinates (outer + holes, unspecified order). */ | ||
| loops: Point2[][]; | ||
| /** Solid fill vs. a line pattern (patterns render as boundary outlines). */ | ||
| solid: boolean; | ||
| } | ||
| type Entity = LineEntity | PolylineEntity | CircleEntity | ArcEntity | EllipseEntity | InsertEntity | TextEntity | SplineEntity | SolidEntity | PointEntity | DimensionEntity | HatchEntity; | ||
| type EntityType = Entity["type"]; | ||
| interface BlockDef { | ||
| name: string; | ||
| basePoint: Point2; | ||
| entities: Entity[]; | ||
| } | ||
| /** A paper-space viewport: a window that frames model space at a fixed scale. */ | ||
| interface Viewport { | ||
| /** Center of the window on the paper (paper coords). */ | ||
| center: Point2; | ||
| /** Window size on the paper. */ | ||
| width: number; | ||
| height: number; | ||
| /** Model point shown at the window center. */ | ||
| viewCenter: Point2; | ||
| /** Model-space height visible through the window (drives the scale). */ | ||
| viewHeight: number; | ||
| /** View twist, radians (CCW). */ | ||
| twist: number; | ||
| } | ||
| /** A paper-space layout: a printable sheet with its own geometry and viewports. */ | ||
| interface Layout { | ||
| name: string; | ||
| /** Drawable paper-space geometry (titleblock, borders, text). */ | ||
| entities: Entity[]; | ||
| /** Windows into model space. */ | ||
| viewports: Viewport[]; | ||
| } | ||
| interface DxfDocument { | ||
| layers: Map<string, LayerInfo>; | ||
| entities: Entity[]; | ||
| blocks: Map<string, BlockDef>; | ||
| /** Linetype definitions from the LTYPE table, keyed by name. */ | ||
| lineTypes: Map<string, LineTypeDef>; | ||
| /** Counts of raw DXF entity types that were skipped by the parser stage. */ | ||
| unsupported: Record<string, number>; | ||
| /** | ||
| * Short drawing-unit label from the header's `$INSUNITS` (e.g. "mm", "in"), | ||
| * or "" when the drawing is unitless or the code is unknown. `parseDxf` | ||
| * always sets it; hand-built documents may omit it (treated as ""). | ||
| */ | ||
| units?: string; | ||
| /** | ||
| * Paper-space layouts, if any. `entities` holds model space; each layout | ||
| * carries its own paper geometry and viewports. `parseDxf` sets it (possibly | ||
| * empty); hand-built documents may omit it (treated as no layouts). | ||
| */ | ||
| layouts?: Layout[]; | ||
| } | ||
| //#endregion | ||
| import { A as Viewport, C as PointEntity, D as TextEntity, E as SplineEntity, O as TextHAlign, S as Point3, T as SolidEntity, _ as LayerInfo, a as Affine2D, b as LineTypeDef, c as Bounds, d as DrawingDocument, f as EllipseEntity, g as InsertEntity, h as HatchEntity, i as toBytes, k as TextVAlign, l as CircleEntity, m as EntityType, n as DrawingSource, o as ArcEntity, p as Entity, r as parseWith, s as BlockDef, t as DrawingParser, u as DimensionEntity, v as Layout, w as PolylineEntity, x as Point2, y as LineEntity } from "./registry-CtIhdVSA.mjs"; | ||
| import { t as DrawingParseError } from "./errors-B0HtqeWu.mjs"; | ||
| //#region src/entity-info.d.ts | ||
@@ -302,3 +84,3 @@ /** | ||
| /** Tessellate a document's model space into per-layer batched geometry. */ | ||
| declare function tessellate(doc: DxfDocument, options?: TessellateOptions): Tessellation; | ||
| declare function tessellate(doc: DrawingDocument, options?: TessellateOptions): Tessellation; | ||
| /** | ||
@@ -310,3 +92,3 @@ * Tessellate a paper-space layout: its own geometry in paper coordinates, | ||
| */ | ||
| declare function tessellateLayout(doc: DxfDocument, layout: Layout, options?: TessellateOptions): Tessellation; | ||
| declare function tessellateLayout(doc: DrawingDocument, layout: Layout, options?: TessellateOptions): Tessellation; | ||
| //#endregion | ||
@@ -347,6 +129,6 @@ //#region src/snap/snap.d.ts | ||
| */ | ||
| declare function buildSnapIndex(tessellation: Tessellation, document: DxfDocument): SnapIndex; | ||
| declare function buildSnapIndex(tessellation: Tessellation, document: DrawingDocument): SnapIndex; | ||
| //#endregion | ||
| //#region src/viewer.d.ts | ||
| interface DxfViewerOptions { | ||
| interface DrawingViewerOptions { | ||
| /** | ||
@@ -359,2 +141,13 @@ * Canvas clear color, 24-bit RGB — or null for a transparent canvas | ||
| curveSegments?: number; | ||
| /** | ||
| * The formats this viewer accepts, tried in order (PARSE-13, VIEW-15). | ||
| * Core imports no parser of its own — pass `dxfParser` from | ||
| * "@aspicio/core/dxf" — which is what keeps a PDF-only app free of DXF | ||
| * code and vice versa (INV-11). | ||
| * | ||
| * The array is read when a load starts, not at construction, so a list | ||
| * that fills in later (a format module imported after the viewer exists) | ||
| * still counts. | ||
| */ | ||
| parsers?: readonly DrawingParser[]; | ||
| } | ||
@@ -391,9 +184,7 @@ interface ViewerStats { | ||
| } | ||
| /** Everything the viewer accepts as a DXF source. */ | ||
| type DxfSource = string | ArrayBuffer | Blob; | ||
| /** | ||
| * The Aspicio viewer facade: owns a canvas inside `container`, renders a | ||
| * DXF document, and exposes layers, camera fitting, and events. | ||
| * drawing document, and exposes layers, camera fitting, and events. | ||
| */ | ||
| declare class DxfViewer { | ||
| declare class DrawingViewer { | ||
| private readonly container; | ||
@@ -418,6 +209,6 @@ private readonly canvas; | ||
| private pendingView; | ||
| document: DxfDocument | null; | ||
| constructor(container: HTMLElement, options?: DxfViewerOptions); | ||
| /** Load a DXF from text, a File/Blob, or an ArrayBuffer (ASCII or binary). */ | ||
| load(source: DxfSource): Promise<void>; | ||
| document: DrawingDocument | null; | ||
| constructor(container: HTMLElement, options?: DrawingViewerOptions); | ||
| /** Load a drawing from text, a File/Blob, an ArrayBuffer, or bytes. */ | ||
| load(source: DrawingSource): Promise<void>; | ||
| /** Swap in a freshly tessellated space: colors, snap index, geometry, fit. */ | ||
@@ -564,53 +355,2 @@ private activate; | ||
| //#endregion | ||
| //#region src/parse/parse.d.ts | ||
| /** | ||
| * A parse failure phrased for a person. dxf-parser's own messages are library | ||
| * internals that mislead — "Empty file" fires for any single-line non-empty | ||
| * input, and "Unexpected end of input: EOF group not read…" is jargon — so we | ||
| * never surface them (PARSE-12). Callers on every surface (viewer, API, MCP) | ||
| * show `message` directly. | ||
| */ | ||
| declare class DxfParseError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| /** Parse DXF text into the normalized Aspicio document model. */ | ||
| declare function parseDxf(text: string): DxfDocument; | ||
| /** | ||
| * Parse a DXF from raw bytes or text. Headless (no DOM/WebGL) — safe in Node | ||
| * and Cloudflare Workers. Use when the source arrives as bytes (e.g. a fetched | ||
| * file); pass a string to parse ASCII DXF text directly. | ||
| * | ||
| * Binary "AutoCAD Binary DXF" input (both the R12 1-byte and R13+ 2-byte code | ||
| * variants) is detected by its sentinel and decoded. Other bytes are decoded | ||
| * as UTF-8, which also covers ASCII; pre-2007 files using an ANSI code page | ||
| * ($DWGCODEPAGE) will decode non-ASCII text as U+FFFD. | ||
| */ | ||
| declare function parseDxfBytes(source: string | ArrayBuffer | Uint8Array): DxfDocument; | ||
| //#endregion | ||
| //#region src/parse/binary.d.ts | ||
| /** | ||
| * Binary DXF support. | ||
| * | ||
| * A DXF file can be encoded as text (the usual group-code/value lines) or as | ||
| * "AutoCAD Binary DXF" — the same records packed as bytes behind a 22-byte | ||
| * sentinel. This module detects the binary form and transcodes it back into the | ||
| * canonical text stream, so the existing text parser handles it unchanged. | ||
| * | ||
| * Two on-disk variants exist and both are supported: | ||
| * - R13+ (AC1012 and later): group codes are 2-byte little-endian. | ||
| * - R12 and earlier: group codes are a single byte, with `0xFF` escaping to a | ||
| * following 2-byte code. | ||
| * The first record is always `0 SECTION`, so the byte after the first `0x00` | ||
| * code distinguishes them: `0x00` (a 2-byte code's high byte) vs. the `S` of | ||
| * "SECTION". | ||
| */ | ||
| /** True when `bytes` begin with the binary-DXF sentinel. */ | ||
| declare function isBinaryDxf(bytes: Uint8Array): boolean; | ||
| /** | ||
| * Transcode a binary DXF (per {@link isBinaryDxf}) into the equivalent | ||
| * group-code/value text that `parseDxf` consumes. Reads defensively: a | ||
| * truncated record ends the stream rather than throwing. | ||
| */ | ||
| declare function binaryDxfToText(bytes: Uint8Array): string; | ||
| //#endregion | ||
| //#region src/describe.d.ts | ||
@@ -630,2 +370,4 @@ /** One layer's entry in a {@link DrawingSummary}. */ | ||
| interface DrawingSummary { | ||
| /** Which format produced this drawing ("dxf", "pdf"), or "" if unknown. */ | ||
| format: string; | ||
| /** Drawing-unit label from `$INSUNITS` (e.g. "mm"), or "" when unitless. */ | ||
@@ -665,3 +407,3 @@ units: string; | ||
| */ | ||
| declare function describeDrawing(doc: DxfDocument, tessellation: Tessellation): DrawingSummary; | ||
| declare function describeDrawing(doc: DrawingDocument, tessellation: Tessellation): DrawingSummary; | ||
| //#endregion | ||
@@ -846,2 +588,2 @@ //#region src/layers.d.ts | ||
| //#endregion | ||
| export { type ArcEntity, type BlockDef, type Bounds, Camera2D, type CircleEntity, type DimensionEntity, type DrawingSummary, type DxfDocument, DxfParseError, type DxfSource, DxfViewer, type DxfViewerOptions, type EllipseEntity, type Entity, type EntityHandler, type EntityHit, type EntityInfo, type EntityType, type FitViewOptions, type GestureOptions, type HatchEntity, type InsertEntity, type LayerGeometry, type LayerInfo, type LayerSummary, type Layout, type LineEntity, type LineTypeDef, type PickedEntity, type Point2, type Point3, type PointEntity, type PolylineEntity, type ShortcutHandlers, type ShortcutViewer, SnapIndex, type SnapKind, type SnapResult, type SolidEntity, type SplineEntity, type SvgExportOptions, type TessellateOptions, type Tessellation, type TessellationContext, type TextEntity, type TextHAlign, type TextLayoutOptions, type TextVAlign, VERSION, type ViewState, type ViewerEvent, type ViewerStats, type Viewport, attachGestures, attachShortcuts, binaryDxfToText, buildSnapIndex, dashPolyline, decodeTextSpecials, describeDrawing, describeEntity, isBinaryDxf, isEmptyLayer, layoutText, niceLength, parseDxf, parseDxfBytes, partitionLayers, pickEntity, pickLayer, registerEntityHandler, sampleSpline, stripMText, tessellate, tessellateLayout, tessellationToSvg, triangulate, unitLabel }; | ||
| export { type ArcEntity, type BlockDef, type Bounds, Camera2D, type CircleEntity, type DimensionEntity, type DrawingDocument, DrawingParseError, type DrawingParser, type DrawingSource, type DrawingSummary, DrawingViewer, type DrawingViewerOptions, type EllipseEntity, type Entity, type EntityHandler, type EntityHit, type EntityInfo, type EntityType, type FitViewOptions, type GestureOptions, type HatchEntity, type InsertEntity, type LayerGeometry, type LayerInfo, type LayerSummary, type Layout, type LineEntity, type LineTypeDef, type PickedEntity, type Point2, type Point3, type PointEntity, type PolylineEntity, type ShortcutHandlers, type ShortcutViewer, SnapIndex, type SnapKind, type SnapResult, type SolidEntity, type SplineEntity, type SvgExportOptions, type TessellateOptions, type Tessellation, type TessellationContext, type TextEntity, type TextHAlign, type TextLayoutOptions, type TextVAlign, VERSION, type ViewState, type ViewerEvent, type ViewerStats, type Viewport, attachGestures, attachShortcuts, buildSnapIndex, dashPolyline, decodeTextSpecials, describeDrawing, describeEntity, isEmptyLayer, layoutText, niceLength, parseWith, partitionLayers, pickEntity, pickLayer, registerEntityHandler, sampleSpline, stripMText, tessellate, tessellateLayout, tessellationToSvg, toBytes, triangulate, unitLabel }; |
+4
-2
| { | ||
| "name": "@aspicio/core", | ||
| "version": "0.11.1", | ||
| "description": "Aspicio — a TypeScript DXF viewer library (WebGL, mobile-first).", | ||
| "version": "0.12.0", | ||
| "description": "Aspicio — a TypeScript DXF and vector-PDF viewer library (WebGL, mobile-first).", | ||
| "homepage": "https://github.com/frontsail-ai/aspicio/tree/master/packages/core#readme", | ||
@@ -20,2 +20,4 @@ "license": "MIT", | ||
| ".": "./dist/index.mjs", | ||
| "./dxf": "./dist/dxf.mjs", | ||
| "./pdf": "./dist/pdf.mjs", | ||
| "./package.json": "./package.json" | ||
@@ -22,0 +24,0 @@ }, |
+27
-11
| # @aspicio/core | ||
| A TypeScript-first 2D DXF viewer for the web: WebGL rendering, layers, | ||
| and mobile-grade gestures behind one small facade. Framework-agnostic — | ||
| A TypeScript-first 2D drawing viewer for the web — DXF and vector PDF: | ||
| WebGL rendering, layers, and mobile-grade gestures behind one small | ||
| facade. Framework-agnostic — | ||
| React bindings live in | ||
@@ -14,8 +15,13 @@ [`@aspicio/react`](https://github.com/frontsail-ai/aspicio/tree/master/packages/react#readme). | ||
| ```ts | ||
| import { DxfViewer } from "@aspicio/core"; | ||
| import { DrawingViewer } from "@aspicio/core"; | ||
| import { dxfParser } from "@aspicio/core/dxf"; | ||
| const viewer = new DxfViewer(container, { background: 0x16181d }); | ||
| const viewer = new DrawingViewer(container, { background: 0x16181d, parsers: [dxfParser] }); | ||
| await viewer.load(file); // File | Blob | ArrayBuffer | DXF text | ||
| ``` | ||
| Formats are opted into by import. The root entry ships no parser, so | ||
| `parsers` is what teaches the viewer to read DXF — and what keeps a bundle | ||
| free of formats it never asked for. | ||
| That alone gives you an interactive preview inside `container`: drag to | ||
@@ -34,3 +40,3 @@ pan, wheel/pinch to zoom (cursor-anchored), Shift+drag or two-finger | ||
| | `loadUrl(url)` | fetch + load; rejects on HTTP errors | | ||
| | `document` | the parsed, normalized `DxfDocument` (or `null`) | | ||
| | `document` | the parsed, normalized `DrawingDocument` (or `null`) | | ||
| | `stats` | `{ entityCount, segmentCount, unsupported }` — unsupported is a per-type count of skipped entities | | ||
@@ -40,4 +46,4 @@ | ||
| 1-byte and R13+ 2-byte code variants). If you parse bytes yourself, | ||
| `isBinaryDxf(bytes)` and `binaryDxfToText(bytes)` are exported to feed the | ||
| binary form into `parseDxf`. | ||
| `isBinaryDxf(bytes)` and `binaryDxfToText(bytes)` are exported from | ||
| `@aspicio/core/dxf` to feed the binary form into `parseDxf`. | ||
@@ -120,3 +126,4 @@ ### Layers | ||
| ```ts | ||
| new DxfViewer(container, { | ||
| new DrawingViewer(container, { | ||
| parsers: [dxfParser], // the formats this viewer accepts, tried in order | ||
| background: 0x16181d, // 24-bit RGB, or null for a transparent canvas | ||
@@ -157,3 +164,3 @@ curveSegments: 72, // arc flattening resolution (segments per full circle) | ||
| The pipeline is `parseDxf → tessellate → render`, and each stage is | ||
| The pipeline is `parse → tessellate → render`, and each stage is | ||
| exported. Add or override an entity type with one handler — no pipeline | ||
@@ -170,4 +177,5 @@ surgery: | ||
| `parseDxf`, `tessellate`, `pickLayer`, `Camera2D`, and `attachGestures` | ||
| are usable stand-alone for custom renderers. | ||
| `parseWith`, `tessellate`, `pickLayer`, `Camera2D`, and `attachGestures` | ||
| are usable stand-alone for custom renderers; `parseDxf` and `parseDxfBytes` | ||
| come from `@aspicio/core/dxf` when you want the DXF parser directly. | ||
@@ -195,1 +203,9 @@ `attachShortcuts(target, viewer, handlers)` adds keyboard shortcuts to a | ||
| ``` | ||
| ## Migrating from 0.x | ||
| Two breaking changes ship together: | ||
| 1. **Formats are opted into by import.** Add `import "@aspicio/core/dxf";` once — | ||
| without it every load fails with an error saying exactly that. | ||
| 2. **Format-neutral names dropped their `Dxf` prefix**: `DxfViewer` → `DrawingViewer`, `DxfViewerOptions` → `DrawingViewerOptions`, `DxfDocument` → `DrawingDocument`, `DxfSource` → `DrawingSource`, `DxfParseError` → `DrawingParseError`. `parseDxf`, `parseDxfBytes`, `binaryDxfToText`, and `isBinaryDxf` keep their names and move to `@aspicio/core/dxf`. |
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
246267
62.66%12
200%5777
74.64%205
8.47%2
100%