|
|
@@ -0,0 +1,270 @@
|
|
|
+// lib/dxf.ts:极简 DXF 解析(只取"画二维预览"需要的实体)。
|
|
|
+//
|
|
|
+// 为什么不引三方库:three 的 examples 里已无 DXFLoader,而 dxf-parser / three-dxf 需要新增依赖;
|
|
|
+// 这里只覆盖常见几何与文字实体(line/polyline/circle/arc/ellipse/text/point),
|
|
|
+// 不支持的实体按个数统计返回(skipped),由界面如实告知——避免"看着像空图"的误导。
|
|
|
+//
|
|
|
+// DXF 是"组码 + 值"交替两行一组的纯文本格式,且 y 轴向上(与 canvas 相反,渲染时翻转)。
|
|
|
+
|
|
|
+export interface DxfPoint {
|
|
|
+ x: number;
|
|
|
+ y: number;
|
|
|
+}
|
|
|
+
|
|
|
+export type DxfEntity =
|
|
|
+ | { kind: 'line'; a: DxfPoint; b: DxfPoint }
|
|
|
+ | { kind: 'polyline'; points: DxfPoint[]; closed: boolean }
|
|
|
+ | { kind: 'circle'; center: DxfPoint; radius: number }
|
|
|
+ | { kind: 'arc'; center: DxfPoint; radius: number; startDeg: number; endDeg: number }
|
|
|
+ | { kind: 'ellipse'; center: DxfPoint; rx: number; ry: number; rotationDeg: number; startDeg: number; endDeg: number }
|
|
|
+ | { kind: 'text'; at: DxfPoint; height: number; text: string; rotationDeg: number }
|
|
|
+ | { kind: 'point'; at: DxfPoint };
|
|
|
+
|
|
|
+export interface DxfBounds {
|
|
|
+ minX: number;
|
|
|
+ minY: number;
|
|
|
+ maxX: number;
|
|
|
+ maxY: number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface DxfParseResult {
|
|
|
+ entities: DxfEntity[];
|
|
|
+ /** 未支持的实体个数(含 INSERT/SPLINE/HATCH/DIMENSION 等) */
|
|
|
+ skipped: number;
|
|
|
+ bounds: DxfBounds | null;
|
|
|
+}
|
|
|
+
|
|
|
+/** 一个实体在解析过程中的原始组码序列(保序,LWPOLYLINE 的顶点要靠顺序成对还原) */
|
|
|
+interface RawEntity {
|
|
|
+ type: string;
|
|
|
+ codes: Array<[number, string]>;
|
|
|
+}
|
|
|
+
|
|
|
+const num = (raw: RawEntity, code: number, fallback = 0): number => {
|
|
|
+ const hit = raw.codes.find((c) => c[0] === code);
|
|
|
+ const v = hit ? Number.parseFloat(hit[1]) : Number.NaN;
|
|
|
+ return Number.isFinite(v) ? v : fallback;
|
|
|
+};
|
|
|
+
|
|
|
+/** 位标志(code 70):第 1 位表示闭合 */
|
|
|
+const isClosed = (raw: RawEntity): boolean => (Math.trunc(num(raw, 70, 0)) & 1) === 1;
|
|
|
+
|
|
|
+const DEG = 180 / Math.PI;
|
|
|
+
|
|
|
+/** 把 LWPOLYLINE 里交替出现的 10/20 组码还原成顶点序列 */
|
|
|
+const polylinePoints = (raw: RawEntity): Array<{ x: number; y: number }> => {
|
|
|
+ const points: Array<{ x: number; y: number }> = [];
|
|
|
+ let pendingX: number | null = null;
|
|
|
+ for (const [code, value] of raw.codes) {
|
|
|
+ if (code === 10) {
|
|
|
+ pendingX = Number.parseFloat(value);
|
|
|
+ } else if (code === 20 && pendingX !== null) {
|
|
|
+ const y = Number.parseFloat(value);
|
|
|
+ if (Number.isFinite(pendingX) && Number.isFinite(y)) points.push({ x: pendingX, y });
|
|
|
+ pendingX = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return points;
|
|
|
+};
|
|
|
+
|
|
|
+/** 按"组码 值"两行一组切分;容错:空行/非数字组码行只跳过该行,不错位后续内容 */
|
|
|
+const toPairs = (dxfText: string): Array<[number, string]> => {
|
|
|
+ const lines = dxfText.split(/\r\n|\r|\n/);
|
|
|
+ const pairs: Array<[number, string]> = [];
|
|
|
+ let pendingCode: number | null = null;
|
|
|
+ for (const line of lines) {
|
|
|
+ if (pendingCode === null) {
|
|
|
+ const code = Number.parseInt(line.trim(), 10);
|
|
|
+ // 组码行必须是整数;空行等异常行忽略,继续等下一个组码
|
|
|
+ if (Number.isFinite(code)) pendingCode = code;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ pairs.push([pendingCode, line]);
|
|
|
+ pendingCode = null;
|
|
|
+ }
|
|
|
+ return pairs;
|
|
|
+};
|
|
|
+
|
|
|
+/** 把原始实体转成可渲染实体;不支持的返回 null(由调用方计入 skipped) */
|
|
|
+const toEntity = (raw: RawEntity, verticesOfPolyline: Array<{ x: number; y: number }> | null): DxfEntity | null => {
|
|
|
+ switch (raw.type) {
|
|
|
+ case 'LINE':
|
|
|
+ return { kind: 'line', a: { x: num(raw, 10), y: num(raw, 20) }, b: { x: num(raw, 11), y: num(raw, 21) } };
|
|
|
+ case 'LWPOLYLINE': {
|
|
|
+ const points = polylinePoints(raw);
|
|
|
+ return points.length >= 2 ? { kind: 'polyline', points, closed: isClosed(raw) } : null;
|
|
|
+ }
|
|
|
+ case 'POLYLINE': {
|
|
|
+ // 顶点在后续的 VERTEX 实体里,由调用方收集后传入
|
|
|
+ const points = verticesOfPolyline ?? [];
|
|
|
+ return points.length >= 2 ? { kind: 'polyline', points, closed: isClosed(raw) } : null;
|
|
|
+ }
|
|
|
+ case 'CIRCLE':
|
|
|
+ return { kind: 'circle', center: { x: num(raw, 10), y: num(raw, 20) }, radius: num(raw, 40) };
|
|
|
+ case 'ARC':
|
|
|
+ return {
|
|
|
+ kind: 'arc',
|
|
|
+ center: { x: num(raw, 10), y: num(raw, 20) },
|
|
|
+ radius: num(raw, 40),
|
|
|
+ startDeg: num(raw, 50),
|
|
|
+ endDeg: num(raw, 51),
|
|
|
+ };
|
|
|
+ case 'ELLIPSE': {
|
|
|
+ // 11/21 是"相对圆心的长轴端点",40 是短长轴比,41/42 是起止参数(弧度,非角度)
|
|
|
+ const mx = num(raw, 11);
|
|
|
+ const my = num(raw, 21);
|
|
|
+ const rx = Math.hypot(mx, my);
|
|
|
+ const ratio = num(raw, 40, 1) || 1;
|
|
|
+ return {
|
|
|
+ kind: 'ellipse',
|
|
|
+ center: { x: num(raw, 10), y: num(raw, 20) },
|
|
|
+ rx,
|
|
|
+ ry: rx * Math.abs(ratio),
|
|
|
+ rotationDeg: Math.atan2(my, mx) * DEG,
|
|
|
+ startDeg: num(raw, 41, 0) * DEG,
|
|
|
+ endDeg: num(raw, 42, Math.PI * 2) * DEG,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'TEXT':
|
|
|
+ case 'MTEXT': {
|
|
|
+ // MTEXT 长文本会拆成 1/3 两个组码,这里拼接
|
|
|
+ const body = raw.codes
|
|
|
+ .filter((c) => c[0] === 1 || c[0] === 3)
|
|
|
+ .map((c) => c[1])
|
|
|
+ .join('');
|
|
|
+ return { kind: 'text', at: { x: num(raw, 10), y: num(raw, 20) }, height: num(raw, 40), text: body, rotationDeg: num(raw, 50) };
|
|
|
+ }
|
|
|
+ case 'POINT':
|
|
|
+ return { kind: 'point', at: { x: num(raw, 10), y: num(raw, 20) } };
|
|
|
+ case 'SOLID':
|
|
|
+ case '3DFACE': {
|
|
|
+ // 四个角点,按闭合折线画(预览足够)
|
|
|
+ const corners = [
|
|
|
+ { x: num(raw, 10), y: num(raw, 20) },
|
|
|
+ { x: num(raw, 11), y: num(raw, 21) },
|
|
|
+ { x: num(raw, 12), y: num(raw, 22) },
|
|
|
+ { x: num(raw, 13), y: num(raw, 23) },
|
|
|
+ ];
|
|
|
+ return { kind: 'polyline', points: corners, closed: true };
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const growBounds = (b: DxfBounds, x: number, y: number): void => {
|
|
|
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
|
+ if (x < b.minX) b.minX = x;
|
|
|
+ if (y < b.minY) b.minY = y;
|
|
|
+ if (x > b.maxX) b.maxX = x;
|
|
|
+ if (y > b.maxY) b.maxY = y;
|
|
|
+};
|
|
|
+
|
|
|
+const entityBounds = (e: DxfEntity, b: DxfBounds): void => {
|
|
|
+ switch (e.kind) {
|
|
|
+ case 'line':
|
|
|
+ growBounds(b, e.a.x, e.a.y);
|
|
|
+ growBounds(b, e.b.x, e.b.y);
|
|
|
+ break;
|
|
|
+ case 'polyline':
|
|
|
+ e.points.forEach((p) => growBounds(b, p.x, p.y));
|
|
|
+ break;
|
|
|
+ case 'circle':
|
|
|
+ case 'arc':
|
|
|
+ growBounds(b, e.center.x - e.radius, e.center.y - e.radius);
|
|
|
+ growBounds(b, e.center.x + e.radius, e.center.y + e.radius);
|
|
|
+ break;
|
|
|
+ case 'ellipse': {
|
|
|
+ const r = Math.max(e.rx, e.ry);
|
|
|
+ growBounds(b, e.center.x - r, e.center.y - r);
|
|
|
+ growBounds(b, e.center.x + r, e.center.y + r);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'text':
|
|
|
+ growBounds(b, e.at.x, e.at.y);
|
|
|
+ // 文字高度也算进范围,避免只有标注的图被"缩放到看不见"
|
|
|
+ growBounds(b, e.at.x + e.text.length * e.height * 0.6, e.at.y + e.height);
|
|
|
+ break;
|
|
|
+ case 'point':
|
|
|
+ growBounds(b, e.at.x, e.at.y);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析 DXF 文本。只读 ENTITIES 段;解析失败/无实体时返回空列表,不抛异常。
|
|
|
+ */
|
|
|
+export const parseDxf = (dxfText: string): DxfParseResult => {
|
|
|
+ const entities: DxfEntity[] = [];
|
|
|
+ let skipped = 0;
|
|
|
+ const pairs = toPairs(dxfText);
|
|
|
+
|
|
|
+ let inEntities = false;
|
|
|
+ let raw: RawEntity | null = null;
|
|
|
+ // POLYLINE 的顶点来自后续 VERTEX 实体,需要跨实体累积
|
|
|
+ let polylineVertices: Array<{ x: number; y: number }> | null = null;
|
|
|
+
|
|
|
+ const flush = () => {
|
|
|
+ if (!raw) return;
|
|
|
+ const built = toEntity(raw, raw.type === 'POLYLINE' ? polylineVertices : null);
|
|
|
+ if (built) entities.push(built);
|
|
|
+ else skipped++;
|
|
|
+ raw = null;
|
|
|
+ polylineVertices = null;
|
|
|
+ };
|
|
|
+
|
|
|
+ for (let i = 0; i < pairs.length; i++) {
|
|
|
+ const [code, value] = pairs[i];
|
|
|
+ const token = value.trim();
|
|
|
+
|
|
|
+ if (code === 0) {
|
|
|
+ // 实体/段的边界
|
|
|
+ if (raw?.type === 'POLYLINE' && token === 'VERTEX') {
|
|
|
+ // 顶点属于父 POLYLINE:这里只收集它的组码,不 flush 父实体
|
|
|
+ const vertex: RawEntity = { type: 'VERTEX', codes: [] };
|
|
|
+ const vCodes: Array<[number, string]> = [];
|
|
|
+ let j = i + 1;
|
|
|
+ while (j < pairs.length && pairs[j][0] !== 0) {
|
|
|
+ vCodes.push(pairs[j]);
|
|
|
+ j++;
|
|
|
+ }
|
|
|
+ vertex.codes = vCodes;
|
|
|
+ const vx = num(vertex, 10);
|
|
|
+ const vy = num(vertex, 20);
|
|
|
+ if (Number.isFinite(vx) && Number.isFinite(vy)) polylineVertices!.push({ x: vx, y: vy });
|
|
|
+ i = j - 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ flush();
|
|
|
+
|
|
|
+ if (token === 'SECTION') {
|
|
|
+ // 下一组是段名
|
|
|
+ const name = pairs[i + 1]?.[1]?.trim();
|
|
|
+ inEntities = name === 'ENTITIES';
|
|
|
+ if (pairs[i + 1]) i++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (token === 'ENDSEC') {
|
|
|
+ inEntities = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!inEntities) continue; // BLOCKS/HEADER 等段内的实体不参与预览
|
|
|
+ raw = { type: token, codes: [] };
|
|
|
+ if (token === 'POLYLINE') polylineVertices = [];
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (raw && inEntities) raw.codes.push([code, value]);
|
|
|
+ }
|
|
|
+ flush();
|
|
|
+
|
|
|
+ let bounds: DxfBounds | null = null;
|
|
|
+ if (entities.length) {
|
|
|
+ bounds = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
|
+ entities.forEach((e) => entityBounds(e, bounds!));
|
|
|
+ if (!Number.isFinite(bounds.minX) || bounds.minX > bounds.maxX) bounds = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return { entities, skipped, bounds };
|
|
|
+};
|