/** * פרסר DXF (ASCII) עצמאי — ללא תלויות, רץ בדפדפן. * * תומך בישויות הנפוצות בשרטוטי תשתית: * LINE, LWPOLYLINE (כולל bulge), POLYLINE/VERTEX, CIRCLE, ARC, ELLIPSE, * SPLINE (קירוב), POINT, TEXT, MTEXT, SOLID, 3DFACE, INSERT (בלוקים, * רקורסיבי), DIMENSION (דרך בלוק אנונימי), HATCH (גבול בלבד). * * הפלט: ישויות מנורמלות בקואורדינטות השרטוט — * { type: 'poly', pts: [[x,y],...], closed, layer, color } * { type: 'point', pt: [x,y], layer, color } * { type: 'text', pt: [x,y], text, height, rotation, layer, color } * color הוא hex לאחר פתרון BYLAYER/BYBLOCK/TrueColor (darkBase=false; * ההצגה מטפלת בהיפוך שחור/לבן לפי הרקע דרך ה-aci המקורי שנשמר). */ (function (global) { "use strict"; var ARC_SEGS = 32; // צפיפות טסלציה לקשתות מלאות // ---------- קריאת תגיות ---------- function tokenize(text) { var lines = text.split(/\r\n|\r|\n/); var tags = []; for (var i = 0; i + 1 < lines.length; i += 2) { var code = parseInt(lines[i], 10); if (isNaN(code)) continue; tags.push([code, lines[i + 1]]); } return tags; } // ---------- מבני עזר ---------- function Cursor(tags) { this.tags = tags; this.i = 0; } Cursor.prototype.peek = function () { return this.tags[this.i]; }; Cursor.prototype.next = function () { return this.tags[this.i++]; }; Cursor.prototype.done = function () { return this.i >= this.tags.length; }; function collectEntityTags(cur) { // אוסף תגיות עד ל-0 הבא (לא כולל) var out = []; while (!cur.done() && cur.peek()[0] !== 0) out.push(cur.next()); return out; } function tagMap(tags) { // מפה של קוד → ערך ראשון, ו-arrays לקודים חוזרים var m = { _all: tags }; for (var i = 0; i < tags.length; i++) { var c = tags[i][0], v = tags[i][1]; if (!(c in m)) m[c] = v; (m["arr" + c] = m["arr" + c] || []).push(v); } return m; } function num(v, dflt) { var f = parseFloat(v); return isNaN(f) ? (dflt || 0) : f; } // ---------- גאומטריה ---------- function arcPoints(cx, cy, r, a0, a1, segsPerFull) { // a0,a1 ברדיאנים, נגד כיוון השעון while (a1 < a0) a1 += Math.PI * 2; var sweep = a1 - a0; var n = Math.max(2, Math.ceil((sweep / (Math.PI * 2)) * (segsPerFull || ARC_SEGS))); var pts = []; for (var i = 0; i <= n; i++) { var a = a0 + (sweep * i) / n; pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); } return pts; } function bulgeToArc(p1, p2, bulge) { // ממיר קטע-עם-bulge לנקודות קשת (כולל p1, לא כולל p2) var theta = 4 * Math.atan(bulge); var dx = p2[0] - p1[0], dy = p2[1] - p1[1]; var chord = Math.sqrt(dx * dx + dy * dy); if (chord < 1e-12) return [p1]; var r = chord / (2 * Math.sin(Math.abs(theta) / 2)); var mx = (p1[0] + p2[0]) / 2, my = (p1[1] + p2[1]) / 2; var h = Math.sqrt(Math.max(0, r * r - (chord / 2) * (chord / 2))); var sign = bulge > 0 ? 1 : -1; var cx = mx - sign * h * (dy / chord); var cy = my + sign * h * (dx / chord); var a0 = Math.atan2(p1[1] - cy, p1[0] - cx); var n = Math.max(2, Math.ceil((Math.abs(theta) / (Math.PI * 2)) * ARC_SEGS)); var pts = []; for (var i = 0; i < n; i++) { var a = a0 + (theta * i) / n; pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); } return pts; } function cleanMtext(s) { return s .replace(/\\\\/g, "\u0001") .replace(/\\P/gi, "\n") .replace(/\\[fF][^;]*;/g, "") .replace(/\\[HhWwQqTtCcAa][0-9.]+x?;?/g, "") .replace(/\\[LlOoKk]/g, "") .replace(/[{}]/g, "") .replace(/%%[dD]/g, "°").replace(/%%[pP]/g, "±").replace(/%%[cC]/g, "Ø") .replace(/%%[uUoO]/g, "") // סימון יוניקוד של AutoCAD (עברית בקבצי ASCII): \U+05D0 ← א .replace(/\\U\+([0-9A-Fa-f]{4})/g, function (m, hex) { return String.fromCharCode(parseInt(hex, 16)); }) .replace(/\u0001/g, "\\"); } // ---------- פירוק ישות בודדת ---------- function parseVertexChain(cur) { // POLYLINE ישן: ישויות VERTEX עד SEQEND var pts = [], bulges = []; while (!cur.done()) { var t = cur.peek(); if (t[0] === 0 && t[1] === "VERTEX") { cur.next(); var m = tagMap(collectEntityTags(cur)); pts.push([num(m[10]), num(m[20])]); bulges.push(num(m[42])); } else if (t[0] === 0 && t[1] === "SEQEND") { cur.next(); collectEntityTags(cur); break; } else break; } return { pts: pts, bulges: bulges }; } function expandBulges(pts, bulges, closed) { var hasBulge = bulges.some(function (b) { return b; }); if (!hasBulge) return pts.slice(); var out = []; var n = pts.length; var last = closed ? n : n - 1; for (var i = 0; i < last; i++) { var p1 = pts[i], p2 = pts[(i + 1) % n], b = bulges[i] || 0; if (b) out = out.concat(bulgeToArc(p1, p2, b)); else out.push(p1); } if (!closed) out.push(pts[n - 1]); return out; } function decodeEntity(type, m, cur, out) { var base = { layer: m[8] || "0", aci: m[62] !== undefined ? parseInt(m[62], 10) : null, // null=BYLAYER, 0=BYBLOCK trueColor: m[420] !== undefined ? parseInt(m[420], 10) : null, }; switch (type) { case "LINE": out.push(ent(base, "poly", { pts: [[num(m[10]), num(m[20])], [num(m[11]), num(m[21])]], closed: false })); break; case "LWPOLYLINE": { var xs = (m.arr10 || []).map(Number), ys = (m.arr20 || []).map(Number); // bulge (42) מיוחס לקודקוד האחרון שנקרא — משחזרים לפי הסדר בתגיות var pts = [], bulges = []; var all = m._all, vi = -1; for (var i = 0; i < all.length; i++) { if (all[i][0] === 10) { pts.push([num(all[i][1]), 0]); bulges.push(0); vi++; } else if (all[i][0] === 20 && vi >= 0) pts[vi][1] = num(all[i][1]); else if (all[i][0] === 42 && vi >= 0) bulges[vi] = num(all[i][1]); } if (pts.length < 2) break; var closed = (parseInt(m[70] || "0", 10) & 1) === 1; out.push(ent(base, "poly", { pts: expandBulges(pts, bulges, closed), closed: closed })); break; } case "POLYLINE": { var flags = parseInt(m[70] || "0", 10); var chain = parseVertexChain(cur); if (chain.pts.length >= 2) { var cl = (flags & 1) === 1; out.push(ent(base, "poly", { pts: expandBulges(chain.pts, chain.bulges, cl), closed: cl })); } break; } case "CIRCLE": out.push(ent(base, "poly", { pts: arcPoints(num(m[10]), num(m[20]), num(m[40]), 0, Math.PI * 2, 48), closed: true, })); break; case "ARC": { var a0 = num(m[50]) * Math.PI / 180, a1 = num(m[51]) * Math.PI / 180; out.push(ent(base, "poly", { pts: arcPoints(num(m[10]), num(m[20]), num(m[40]), a0, a1, 48), closed: false })); break; } case "ELLIPSE": { var cx = num(m[10]), cy = num(m[20]); var mx = num(m[11]), my = num(m[21]); // וקטור ציר ראשי var ratio = num(m[40], 1); var t0 = num(m[41], 0), t1 = num(m[42], Math.PI * 2); while (t1 < t0) t1 += Math.PI * 2; var n = Math.max(8, Math.ceil(((t1 - t0) / (Math.PI * 2)) * 48)); var e = []; for (var k = 0; k <= n; k++) { var t = t0 + ((t1 - t0) * k) / n; var ex = Math.cos(t), ey = Math.sin(t) * ratio; e.push([cx + ex * mx - ey * my, cy + ex * my + ey * mx]); } out.push(ent(base, "poly", { pts: e, closed: Math.abs(t1 - t0 - Math.PI * 2) < 1e-6 })); break; } case "SPLINE": { // קירוב: fit points אם קיימים, אחרת פוליגון נקודות הבקרה var fx = m.arr11 || [], fy = m.arr21 || []; var sx = fx.length >= 2 ? fx : (m.arr10 || []); var sy = fx.length >= 2 ? fy : (m.arr20 || []); if (sx.length >= 2) { var sp = []; for (var j = 0; j < sx.length; j++) sp.push([num(sx[j]), num(sy[j])]); out.push(ent(base, "poly", { pts: sp, closed: (parseInt(m[70] || "0", 10) & 1) === 1 })); } break; } case "POINT": out.push(ent(base, "point", { pt: [num(m[10]), num(m[20])] })); break; case "TEXT": if ((m[1] || "").trim()) { out.push(ent(base, "text", { pt: [num(m[10]), num(m[20])], text: cleanMtext(m[1]), height: num(m[40], 1), rotation: num(m[50], 0), })); } break; case "MTEXT": { var txt = (m.arr3 || []).join("") + (m[1] || ""); txt = cleanMtext(txt).trim(); if (txt) { out.push(ent(base, "text", { pt: [num(m[10]), num(m[20])], text: txt, height: num(m[40], 1), rotation: num(m[50], 0), })); } break; } case "SOLID": case "3DFACE": { var q = [[num(m[10]), num(m[20])], [num(m[11]), num(m[21])], [num(m[13]), num(m[23])], [num(m[12]), num(m[22])]]; out.push(ent(base, "poly", { pts: q, closed: true })); break; } case "INSERT": case "DIMENSION": { out.push(ent(base, "insert", { block: m[2] || "", pt: [num(m[10]), num(m[20])], sx: num(m[41], 1) || 1, sy: num(m[42], 1) || 1, rotation: num(m[50], 0), isDim: type === "DIMENSION", })); break; } case "HATCH": { // גבול בלבד: polyline boundary path (השמטה שקטה של גבולות edge מורכבים) var all2 = m._all, hp = [], inLoop = false, nPts = 0; for (var h = 0; h < all2.length; h++) { var c = all2[h][0], v = all2[h][1]; if (c === 93) { nPts = parseInt(v, 10); inLoop = true; hp = []; } else if (inLoop && c === 10) hp.push([num(v), 0]); else if (inLoop && c === 20 && hp.length) { hp[hp.length - 1][1] = num(v); if (hp.length === nPts) { if (hp.length >= 3) out.push(ent(base, "poly", { pts: hp.slice(), closed: true })); inLoop = false; } } } break; } } } function ent(base, type, extra) { var e = { type: type, layer: base.layer, aci: base.aci, trueColor: base.trueColor }; for (var k in extra) e[k] = extra[k]; return e; } // ---------- פירוק המסמך ---------- function parseTags(tags) { var cur = new Cursor(tags); var doc = { header: {}, layers: {}, blocks: {}, entities: [] }; var section = null; while (!cur.done()) { var t = cur.next(); if (t[0] !== 0) continue; if (t[1] === "SECTION") { var st = cur.peek(); section = st && st[0] === 2 ? st[1] : null; if (st) cur.next(); if (section === "HEADER") parseHeader(cur, doc); else if (section === "TABLES") parseTables(cur, doc); else if (section === "BLOCKS") parseBlocks(cur, doc); else if (section === "ENTITIES") parseEntities(cur, doc.entities); } } return doc; } function parseHeader(cur, doc) { var varName = null; while (!cur.done()) { var t = cur.next(); if (t[0] === 0 && t[1] === "ENDSEC") break; if (t[0] === 9) varName = t[1]; else if (varName) { if (!(varName in doc.header)) doc.header[varName] = t[1]; else if (varName === "$EXTMIN" || varName === "$EXTMAX" || varName === "$INSBASE") { doc.header[varName] += "," + t[1]; } } } } function parseTables(cur, doc) { while (!cur.done()) { var t = cur.next(); if (t[0] === 0 && t[1] === "ENDSEC") break; if (t[0] === 0 && t[1] === "LAYER") { var m = tagMap(collectEntityTags(cur)); if (m[2]) { var aci = m[62] !== undefined ? parseInt(m[62], 10) : 7; doc.layers[m[2]] = { aci: Math.abs(aci) || 7, off: aci < 0, frozen: (parseInt(m[70] || "0", 10) & 1) === 1, }; } } } } function parseBlocks(cur, doc) { var current = null; while (!cur.done()) { var t = cur.next(); if (t[0] === 0 && t[1] === "ENDSEC") break; if (t[0] === 0 && t[1] === "BLOCK") { var m = tagMap(collectEntityTags(cur)); current = { base: [num(m[10]), num(m[20])], entities: [] }; if (m[2]) doc.blocks[m[2]] = current; } else if (t[0] === 0 && t[1] === "ENDBLK") { collectEntityTags(cur); current = null; } else if (t[0] === 0 && current) { var type = t[1]; var mm = tagMap(collectEntityTags(cur)); decodeEntity(type, mm, cur, current.entities); } } } function parseEntities(cur, out) { while (!cur.done()) { var t = cur.next(); if (t[0] === 0 && t[1] === "ENDSEC") break; if (t[0] === 0) { var m = tagMap(collectEntityTags(cur)); decodeEntity(t[1], m, cur, out); } } } // ---------- שטיחת בלוקים ופתרון צבעים ---------- function xform(pt, ins, blockBase) { var x = (pt[0] - blockBase[0]) * ins.sx; var y = (pt[1] - blockBase[1]) * ins.sy; var a = ins.rotation * Math.PI / 180; if (a) { var c = Math.cos(a), s = Math.sin(a); var rx = x * c - y * s, ry = x * s + y * c; x = rx; y = ry; } return [x + ins.pt[0], y + ins.pt[1]]; } function resolveColor(e, layers, parentAci, darkBase) { if (e.trueColor !== null && e.trueColor !== undefined) { return "#" + ("000000" + e.trueColor.toString(16)).slice(-6); } var aci = e.aci; if (aci === 0) aci = parentAci || 7; // BYBLOCK if (aci === null || aci === undefined || aci === 256) { // BYLAYER var lay = layers[e.layer]; aci = lay ? lay.aci : 7; } return global.ACI.toHex(Math.abs(aci) || 7, !!darkBase); } function flatten(doc, opts) { opts = opts || {}; var out = []; var layers = doc.layers; function emit(e, transform, parentAci, parentLayer, depth) { var layer = e.layer === "0" && parentLayer ? parentLayer : e.layer; if (e.type === "insert") { if (depth > 8) return; var block = doc.blocks[e.block]; if (!block) return; if (depth === 0) { // רשומת-עזר לא-מצוירת: נקודת ההכנסה ושם הבלוק — משמשת את // מצב שיוך-הנכסים לאיתור עמודים/ארונות (POLE/ZBOX וכו') var lay0 = layers[layer]; if (!lay0 || (!lay0.off && !lay0.frozen)) { out.push({ type: "blockref", block: e.block, layer: layer, pt: transform(e.pt) }); } } var insAci = e.aci === null || e.aci === undefined ? (layers[layer] ? layers[layer].aci : 7) : (e.aci === 0 ? parentAci : e.aci); for (var i = 0; i < block.entities.length; i++) { emit(block.entities[i], function (pt) { return transform(xform(pt, e, block.base)); }, insAci, layer, depth + 1); } return; } var lay = layers[layer]; if (lay && (lay.off || lay.frozen)) return; var copy = { type: e.type, layer: layer, aciResolved: null, color: resolveColor({ aci: e.aci, trueColor: e.trueColor, layer: layer }, layers, parentAci, false), colorDark: resolveColor({ aci: e.aci, trueColor: e.trueColor, layer: layer }, layers, parentAci, true), }; if (e.type === "poly") { copy.pts = e.pts.map(transform); copy.closed = e.closed; } else if (e.type === "point") { copy.pt = transform(e.pt); } else if (e.type === "text") { copy.pt = transform(e.pt); copy.text = e.text; copy.height = e.height; copy.rotation = e.rotation; } out.push(copy); } var ident = function (pt) { return pt; }; for (var i = 0; i < doc.entities.length; i++) emit(doc.entities[i], ident, 7, null, 0); return out; } function bbox(entities) { var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; function acc(p) { if (!isFinite(p[0]) || !isFinite(p[1])) return; if (p[0] < minX) minX = p[0]; if (p[1] < minY) minY = p[1]; if (p[0] > maxX) maxX = p[0]; if (p[1] > maxY) maxY = p[1]; } entities.forEach(function (e) { if (e.pts) e.pts.forEach(acc); else if (e.pt) acc(e.pt); }); if (minX === Infinity) return null; return { minX: minX, minY: minY, maxX: maxX, maxY: maxY }; } // ---------- קידוד קבצים בעברית ---------- function decodeBuffer(buf) { var utf8 = new TextDecoder("utf-8", { fatal: false }).decode(buf); var cpMatch = utf8.match(/\$DWGCODEPAGE[\s\S]{0,40}?\n\s*3\s*\n\s*(\S+)/i); var cp = cpMatch ? cpMatch[1].toUpperCase() : ""; var hasBad = utf8.indexOf("�") !== -1; if (hasBad || /1255/.test(cp)) { try { var heb = new TextDecoder("windows-1255", { fatal: false }).decode(buf); if (/1255/.test(cp) || heb.indexOf("�") === -1) return heb; } catch (e) { /* דפדפן ללא תמיכה — נשארים עם utf-8 */ } } return utf8; } function parse(arrayBuffer) { var head = new Uint8Array(arrayBuffer.slice(0, 6)); var magic = String.fromCharCode.apply(null, head); if (magic.indexOf("AC10") === 0 || magic.indexOf("AC12") === 0 || magic.indexOf("AC14") === 0 || magic.indexOf("AC15") === 0 || magic.indexOf("AC18") === 0 || magic.indexOf("AC2") === 0 || magic.indexOf("AC30") === 0 || magic.indexOf("AC32") === 0) { throw new Error("DWG_BINARY"); } var text = decodeBuffer(arrayBuffer); if (text.indexOf("SECTION") === -1) throw new Error("NOT_DXF"); var doc = parseTags(tokenize(text)); var entities = flatten(doc); if (!entities.length) throw new Error("EMPTY_DXF"); return { header: doc.header, layerNames: Object.keys(doc.layers), entities: entities, bbox: bbox(entities), units: parseInt(doc.header.$INSUNITS || "0", 10), }; } /** * פענוח א-סינכרוני ב-Web Worker — לא מקפיא את הממשק בקבצים כבדים. * נופל חזרה לפענוח סינכרוני אם Worker לא זמין. * שים לב: ה-ArrayBuffer מועבר (transfer) ואינו שמיש אחרי הקריאה. */ function parseAsync(arrayBuffer) { return new Promise(function (resolve, reject) { if (typeof Worker === "undefined" || typeof document === "undefined") { try { resolve(parse(arrayBuffer)); } catch (e) { reject(e); } return; } var w; try { w = new Worker("js/parse-worker.js"); } catch (e) { try { resolve(parse(arrayBuffer)); } catch (err) { reject(err); } return; } w.onmessage = function (ev) { w.terminate(); if (ev.data && ev.data.ok) resolve(ev.data.result); else reject(new Error(ev.data && ev.data.error ? ev.data.error : "PARSE_ERROR")); }; w.onerror = function () { w.terminate(); reject(new Error("WORKER_ERROR")); }; w.postMessage(arrayBuffer, [arrayBuffer]); }); } global.DXF = { parse: parse, parseAsync: parseAsync, bbox: bbox }; })(typeof window !== "undefined" ? window : self);