|
|
@@ -1,270 +0,0 @@
|
|
|
-// 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 };
|
|
|
-};
|