前言 用數據生成CAD圖,一般採用的ObjectArx對CAD二次開發完成。ObjectARX是AutoDesk公司針對AutoCAD平臺上的二次開發而推出的一個開發軟體包,它提供了以C++為基礎的面向對象的開發環境及應用程式介面,能訪問和創建AutoCAD圖形資料庫。而由於現在懂C++的人少,很多 ...
用數據生成CAD圖,一般採用的ObjectArx對CAD二次開發完成。ObjectARX是AutoDesk公司針對AutoCAD平臺上的二次開發而推出的一個開發軟體包,它提供了以C++為基礎的面向對象的開發環境及應用程式介面,能訪問和創建AutoCAD圖形資料庫。而由於現在懂C++的人少,很多人對C++有點望而生畏。則JavaScript 是互聯網上最流行的腳本語言,用戶群體很大。那有沒有可能利用JavaScript來進行數據成圖?
今天和大家聊聊,怎麼用500行JavaScript代碼,根據數據在前端創建一個Dwg格式的工程剖面圖。
效果
先上效果圖
它支持哪些功能?
-
支持CAD的27種實體類型的創建,如線、文字、填充等
-
支持對DWG圖中實體進行修改、克隆、刪除等操作
-
支持創建CAD圖層、線型、塊定義、文字樣式
-
支持從外部圖形中拷貝實體到當前創建的CAD圖中
-
支持塊屬性文字的創建和設置
-
對創建好的CAD圖形數據能以GeoJson的格式在前端直接展示,同時能選中移動等操作
-
對創建好的CAD圖形能在前端展示,同時能點擊彈出實體類型等屬性
-
能導出成DWG圖形
實現原理
(1) 對剖面圖中不變的元素如圖例做成模板。創建圖時直接拷貝這些實體即可。對於圖簽可以外部圖形插入,同時圖簽中需要修改的文字內容如製圖人或日期等欄位,可以塊屬性文字的方式在創建時以屬性賦值的方式來進行創建。如上面生成的剖面圖的模板來源於下麵這兩個模板圖形。 剖面圖模板:
圖簽模板:(其中單位和日期是塊屬性文字,支持插入的時候輸入屬性值進行修改)
(2) 獲取要創建的繪圖數據,示例中對數據進行了模擬生成。
(3) 根據唯傑地圖
如何減少代碼量可以用如下方法:
-
技巧一:可以直接拷貝模板中的實體,對實體的屬性進行修改。這樣能少賦值參數,減少代碼量。
-
技巧二:對於重覆的對象,可以創建塊,變化的文字,以塊屬性文字定義。再重覆創建塊參照,修改屬性文字。
(4) 把創建的數據生成一個JSON對象,調用唯傑地圖服務,後臺創建DWG圖形。
(5) 把後臺創建的DWG圖形數據以GeoJson數據或GIS瓦片的格式返回給前端進行展示。對於圖不大的情況,可用GeoJson數據進行展示。如果圖大時,GeoJson數據量大,數據返回慢,渲染也會受影響,這時建議用GIS柵格瓦片或矢量瓦片的時候進行繪製。
線上體驗地址
應用場景
能在前端通過JavaScript創建CAD格式的DWG圖形,極大的降低了數據生成CAD圖的門檻,具有很廣泛的應用場景。例如,在建築和工程領域,DWG文件是廣泛使用的標準文件格式,如工程中常用的一些等值線圖、剖面圖、水點陣圖等;建築、交通等不同行業中的相關圖紙都可以用這個來生成DWG圖形。偷個懶,讓目前很火的ChatGPT來總結下吧:
全部實現代碼
// --數據自動生成CAD工程剖面圖--根據數據在前端創建生成CAD格式的工程剖面圖形 // 剖面圖模板來源地圖id和版本 let templateSectId = "template_sect"; let templateSecVersion = "v1"; // 圖框模板來源id和版本 const templateTkMapId = "template_tk"; const templateTkVersion = "v1"; // 註:以下所的有objectid來源方法為: // 在唯傑雲端管理平臺 https://vjmap.com/app/cloud 裡面以記憶體方式打開模板圖,然後點擊相應實體,在屬性面板中獲取object值 // 或者以幾何渲染方式打開模板圖,點擊相應實體,在屬性面板中獲取object值,如果是塊實體(objectid中有多個_),取第一個_前面的字元串 let svc = new vjmap.Service(env.serviceUrl, env.accessToken); // 獲取模板信息 let tplInfo; // 獲取模板中的信息 const getTemplateInfo = async (templateSectId, version) => { let features = await getTemplateData(templateSectId, version); // 獲取所有填充符號。先獲取 填充符號 圖層中的所有文字,文字上面的hatch就是填充符號 let hatchInfos = features.filter(f => f.layername == "填充符號" && f.name == "AcDbMText").map(t => { let hatch = features.filter(f => f.layername == "填充符號" && f.name == "AcDbHatch").find(h => // 填充垂直方向位於文字上方,並且距離不能超過文字高度兩倍,水平方向包含文字中心點水平方向 h.envelop.min.y > t.envelop.max.y && h.envelop.min.y - t.envelop.max.y < t.envelop.height() * 2 && h.envelop.min.x <= t.envelop.center().x && h.envelop.max.x >= t.envelop.center().x ) if (!hatch) return; return { name: t.text, hatchObjectId: hatch.objectid } }) // 獲取繪製開始的位置線 let lineInfo = features.filter(f => f.layername == "線" && f.name == "AcDbLine"); let startLine; if (lineInfo.length > 0) { startLine = { objectId: lineInfo[0].objectid, positon: [lineInfo[0].envelop.min.x, lineInfo[0].envelop.min.y] } } return { startLine, hatchInfos } } // 模擬數據 const mockData = (hatchNames, minCount) => { // 對填充符號次序先隨機排序下,這樣每次生成次序就不一樣了 hatchNames.sort(() => Math.random() - 0.5); let data = []; // 孔口個數 let kongCount = vjmap.randInt(minCount, minCount * 2); for(let i = 0; i < kongCount; i++) { let item = { name: '孔' + (i + 1), x: 15 * (i + 1) + vjmap.randInt(0, 10) + 1000, // 孔口坐標x 生成隨機數x y: vjmap.randInt(100, 105), // 孔口坐標y 生成隨機數y stratums: [] // 分層數據 } // 生成每層的信息 let stratumCount = vjmap.randInt(5, hatchNames.length - 1); let stratumAllThickness = 0; for(let k = 0; k < stratumCount; k++) { const thickness = vjmap.randInt(2, 6) // 隨機生成一個厚度 item.stratums.push({ hatch: hatchNames[k], thickness: thickness }) stratumAllThickness += thickness; } item.stratumsThickness = stratumAllThickness; // 所有的厚度 data.push(item); } return data; } // 創建剖面圖 const createSectDoc = async (sectData) => { // 獲取要繪製的數據 let drawData = sectData; // 獲取最大和最小值 let minX = Math.min(...drawData.map(d => d.x)); let maxX = Math.max(...drawData.map(d => d.x)); let minY = Math.min(...drawData.map(d => d.y)); let maxY = Math.max(...drawData.map(d => d.y + d.stratumsThickness)); minY = Math.floor(minY / 10) * 10; // 往10取整,刻度以10為單位 maxY = Math.ceil(maxY / 10) * 10 + 10; // 往10取整,刻度以10為單位,稍長點 let posMaxX = maxX - minX + 20; //x繪製位置,相對距離從標尺偏移十個像素 let posMinX = 10;//x繪製位置,相對距離從標尺偏移十個像素 const startPoint = tplInfo.startLine.positon; let doc = new vjmap.DbDocument(); // 數據來源 doc.from = `${templateSectId}/${templateSecVersion}`; // 把來源圖的數據最後都清空,(這裡的模板不需要清空,直接用了) // doc.isClearFromDb = true; let entitys = []; // 左邊刻度 entitys.push(new vjmap.DbLine({ objectid: "169A2", start: startPoint, end: [startPoint[0], startPoint[1] + (maxY - minY)] })) for(let y = minY; y < maxY; y += 10) { let pt = [startPoint[0], startPoint[1] + maxY - y]; entitys.push(new vjmap.DbLine({ start: pt, end: [pt[0] - 2, pt[1]] })) // 刻度值 entitys.push(new vjmap.DbText({ cloneObjectId: '168C8', position: [pt[0] - 1, pt[1] + 0.2], text: y + '' })) } // 右邊刻度 entitys.push(new vjmap.DbLine({ cloneObjectId: "169A2", // 不是修改了,是克隆左邊的刻度線 start: [startPoint[0] + posMaxX, startPoint[1]], end: [startPoint[0] + posMaxX, startPoint[1] + (maxY - minY)] })) for(let y = minY; y < maxY; y += 10) { let pt = [startPoint[0], startPoint[1] + maxY - y]; entitys.push(new vjmap.DbLine({ start: [pt[0] + posMaxX , pt[1]], end: [pt[0] + posMaxX + 2, pt[1]] })) // 刻度值 entitys.push(new vjmap.DbText({ cloneObjectId: '168C8', position: [pt[0] + posMaxX + 1, pt[1] + 0.2], text: y + '' })) } // 修改線坐標 entitys.push(new vjmap.DbLine({ cloneObjectId: tplInfo.startLine.objectId, start: [startPoint[0], startPoint[1]], end: [startPoint[0] + posMaxX, startPoint[1]] })) // 演示下塊及屬性欄位的使用,這裡用塊創建一個孔口名稱和x坐標,中間用橫線隔開 const blockName = "nameAndx"; let block = new vjmap.DbBlock(); block.name = blockName; block.origin = [0, 0] block.entitys = [ new vjmap.DbAttributeDefinition({ position: [0, 0.2], contents: "名稱", tag: "NAME", colorIndex: 7, // 自動反色 horizontalMode: vjmap.DbTextHorzMode.kTextCenter, verticalMode: vjmap.DbTextVertMode.kTextBottom, // kTextBottom, height: 0.5, }), new vjmap.DbLine({ start: [-2, 0], end: [2, 0] }), new vjmap.DbAttributeDefinition({ position: [0, -0.2], contents: "X坐標", tag: "POSX", colorIndex: 7, // 自動反色 horizontalMode: vjmap.DbTextHorzMode.kTextCenter, verticalMode: vjmap.DbTextVertMode.kTextTop, // kTextBottom, height: 0.5, }) ]; doc.appendBlock(block); // 繪製每一個孔 for(let i = 0; i < drawData.length; i++) { // 開始繪製的位置點 let x = posMinX + drawData[i].x - minX; let y = startPoint[1] + maxY - drawData[i].y; // 名稱和x,用上面的塊創建塊參照 let blockRef = new vjmap.DbBlockReference(); blockRef.blockname = blockName; blockRef.position = [x + 1.5, y + 3]; // 修改屬性定義值 blockRef.attribute = { NAME: drawData[i].name, POSX: drawData[i].x } entitys.push(blockRef); // 一層一層繪製 for(let k = 0; k < drawData[i].stratums.length; k++) { let y2 = y - drawData[i].stratums[k].thickness; let bounds = vjmap.GeoBounds.fromArray([x, y, x + 3, y2]); let points = bounds.toPointArray(); // 轉成點坐標格式 // 閉合 points.push(points[0]); // 填充 entitys.push(new vjmap.DbHatch({ cloneObjectId: drawData[i].stratums[k].hatch.hatchObjectId, points: points, patternScale: 1.5 })) // 邊框 entitys.push(new vjmap.Db2dPolyline({ points: points })) // 繪製連接下一個孔的線 if (i != drawData.length - 1) { const nextKongStratums = drawData[i + 1].stratums; let nextX = posMinX + drawData[i + 1].x - minX; let nextY = startPoint[1] + maxY - drawData[i + 1].y; if (k < nextKongStratums.length) { for(let n = 0; n <= k; n++) { nextY = nextY - drawData[i + 1].stratums[n].thickness; } entitys.push(new vjmap.DbLine({ start: [x + 3, y2], end: [nextX, nextY] })) } // 水平間距 entitys.push(new vjmap.DbLine({ start: [x, startPoint[1]], end: [x, startPoint[1] - 2] })) entitys.push(new vjmap.DbLine({ start: [nextX, startPoint[1]], end: [nextX, startPoint[1] - 2] })) entitys.push(new vjmap.DbLine({ start: [x, startPoint[1] - 2], end: [nextX, startPoint[1] - 2] })) // 間距值 entitys.push(new vjmap.DbText({ cloneObjectId: '168C8', position: [(x + nextX) / 2, startPoint[1] - 1], text: nextX - x, horizontalMode: vjmap.DbTextHorzMode.kTextCenter, // kTextCenter verticalMode: vjmap.DbTextVertMode.kTextVertMid // kTextVertMid, })) } y = y2; } // 最下麵寫上累計厚度值 entitys.push(new vjmap.DbText({ cloneObjectId: '168C8', position: [x + 1.5, y - 0.2], text: drawData[i].stratumsThickness, horizontalMode: vjmap.DbTextHorzMode.kTextCenter, // kTextCenter verticalMode: vjmap.DbTextVertMode.kTextTop // kTextTop, })) } entitys.push(new vjmap.DbText({ objectid: '1687C', position: [(posMinX + posMaxX) / 2.0, startPoint[1] + maxY - minY + 10], /* 如果是相對位置,可以利用矩陣 matrix: [ { op: "translation", vector: [相對偏移x, 相對偏移y] } ],*/ text: `剖面圖${Date.now()}` })) // 繪製圖框 let bounds = vjmap.GeoBounds.fromArray([posMinX - 20, startPoint[1] + maxY - minY + 15, posMaxX + 10, startPoint[1] - 20]); let labelPos = [bounds.max.x, bounds.min.y]; let points = bounds.toPointArray(); // 轉成點坐標格式 // 閉合 points.push(points[0]); // 邊框 entitys.push(new vjmap.Db2dPolyline({ points: points })) bounds = bounds.scale(1.02); points = bounds.toPointArray(); // 轉成點坐標格式 // 閉合 points.push(points[0]); // 邊框 entitys.push(new vjmap.Db2dPolyline({ points: points, lineWidth: 30 // mm })) let date = new Date(); // 圖框從其他模板插入,並修改塊屬性文字 entitys.push(new vjmap.DbBlockReference({ cloneObjectId: '6A1', cloneFromDb: `${templateTkMapId}/${templateTkVersion}`, position: labelPos, attribute: { // 修改塊中的屬性欄位 DATETIME: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`, COMPANY: { text: "唯傑地圖VJMAP", color: 0x00FFFF } } })) entitys.push(new vjmap.DbLine({ objectid: "168C8", // 這個模板文字不用了,直接刪除了 delete: true })) doc.entitys = entitys; return doc; } // 先得設置一個要圖形的所有範圍,這個範圍是隨便都沒有有關係的。最後導出dwg時,會根據實體的所有坐標去自動計算真實的範圍。 let mapBounds = '[-10000,-10000,10000,10000]' let mapExtent = vjmap.GeoBounds.fromString(mapBounds); mapExtent = mapExtent.square(); // 要轉成正方形 svc.setCurrentMapParam({ darkMode: true, // 由於沒有打開過圖,所以主動設置黑色模式 bounds: mapExtent.toString() }) // 建立坐標系 let prj = new vjmap.GeoProjection(mapExtent); // 新建地圖對象 let map = new vjmap.Map({ container: 'map', // container ID style: { version: svc.styleVersion(), glyphs: svc.glyphsUrl(), sources: {}, layers: [] },// 矢量瓦片樣式 center: [0,0], // 中心點 zoom: 2, renderWorldCopies: false }); // 地圖關聯服務對象和坐標系 map.attach(svc, prj); // 使地圖全部可見 map.fitMapBounds(); await map.onLoad(); // 創建一個幾何對象 const createGeomData = async (map, doc) => { let svc = map.getService(); let res = await svc.cmdCreateEntitiesGeomData({ filedoc: doc.toDoc() }); if (res.error) { message.error(res.error); return { type: "FeatureCollection", features: [] }; } if (res.metadata && res.metadata.mapBounds) { // 如果返回的元數據裡面有當前地圖的範圍,則更新當前地圖的坐標範圍 map.updateMapExtent(res.metadata.mapBounds); } const features = []; if (res && res.result && res.result.length > 0) { for (let ent of res.result) { if (ent.geom && ent.geom.geometries) { let clr = map.entColorToHtmlColor(ent.color); // 實體顏色轉html顏色 let featureAttr = {}; // 因為要組合成一個組合實體,所以線和多邊形的顏色得區分 if (ent.isPolygon) { featureAttr.color = clr; // 填充色,只對多邊形有效 featureAttr.noneOutline = true; // 不顯示多邊形邊框,只對多邊形有效 } else { featureAttr.color = clr; // 顏色 featureAttr.line_width = ent.lineWidth; // 線寬 } let ft = { id: vjmap.RandomID(10), type: "Feature", properties: { objectid: ent.objectid, opacity: ent.alpha / 255, ...featureAttr, } } if (ent.geom.geometries.length == 1) { features.push({ ...ft, geometry: ent.geom.geometries[0], }); } else { features.push({ ...ft, geometry: { geometries: ent.geom.geometries, type: "GeometryCollection" }, }); } } } } return { type: "FeatureCollection", features: features, }; }; // 清空之前的地圖數據 const clearMapData = () => { svc.setCurrentMapParam({ darkMode: true, // 由於沒有打開過圖,所以主動設置黑色模式 bounds: mapExtent.toString() }) map.disableLayerClickHighlight(); map.removeDrawLayer(); let sources = map.getStyle().sources; for(let source in sources) { map.removeSourceEx(source); } } // 創建一個有數據的地圖 const createDataMap = async (doc) => { clearMapData(); let geojson = await createGeomData(map, doc); const opts = vjmap.Draw.defaultOptions(); // 修改預設樣式,把點的半徑改成1,沒有邊框,預設為5 let pointIdx = opts.styles.findIndex(s => s.id === "gl-draw-point-point-stroke-inactive"); if (pointIdx >= 0) { opts.styles[pointIdx]['paint']['circle-radius'][3][3] = 0 } pointIdx = opts.styles.findIndex(s => s.id === "gl-draw-point-inactive"); if (pointIdx >= 0) { opts.styles[pointIdx]['paint']['circle-radius'][3][3] = 1 } map.getDrawLayer(opts).set(geojson); } // 創建一個dwg的地圖 const createDwgMap = async (doc) => { // 先清空之前繪製的 clearMapData(); // js代碼 let res = await svc.updateMap({ // 獲取一個臨時的圖id(臨時圖形只會用臨時查看,過期會自動刪除) mapid: vjmap.getTempMapId(1), // 臨時圖形不瀏覽情況下過期自動刪除時間,單位分鐘。預設30 filedoc: doc.toDoc(), mapopenway: vjmap.MapOpenWay.Memory, style: { backcolor: 0 // 如果div背景色是淺色,則設置為oxFFFFFF } }) if (res.error) { message.error(res.error) } await map.switchMap(res); } let curDoc; const exportDwgOpen = async () => { if (!curDoc) return; const mapid = 'exportdwgmap'; let res = await svc.updateMap({ mapid: mapid, filedoc: curDoc.toDoc(), mapopenway: vjmap.MapOpenWay.Memory, style: { backcolor: 0 // 如果div背景色是淺色,則設置為oxFFFFFF } }) if (res.error) { message.error(res.error) } else{ window.open(`https://vjmap.com/app/cloud/#/map/${res.mapid}?version=${res.version}&mapopenway=Memory&vector=false`) } } // 獲取模板的所有數據 const getTemplateData = async (mapid, version) => { let res = await svc.rectQueryFeature({ mapid, version, fields: "", geom: false, // 以記憶體方式打開,獲取真正的objectid maxGeomBytesSize: 0, // 不需要幾何坐標 useCache: true, // 因為是以記憶體方式打開,後臺先把查詢的數據保存進緩存,下次直接去緩存查找,提高效率 // x1,y1,x2,y2同時不輸的話,表示是查詢整個圖的範圍 這範圍不輸入,表示是全圖範圍 }) // 把實體的範圍字元串轉成對象 res.result.map(f => f.envelop = vjmap.GeoBounds.fromString(f.bounds)); console.log(res.result) return res.result; } const creatSectDataMap = async () => { let sectData = mockData(tplInfo.hatchInfos, 5); const doc = await createSectDoc(sectData); await createDataMap(doc); map.fitMapBounds(); curDoc = doc; } const creatSectDwgMap = async () => { let sectData = mockData(tplInfo.hatchInfos, 15); const doc = await createSectDoc(sectData); await createDwgMap(doc); map.fitMapBounds(); // 點擊有高亮狀態(滑鼠點擊地圖元素上時,會高亮) map.enableLayerClickHighlight(svc, e => { if (!e) return; let msg = { content: `type: ${e.name}, id: ${e.objectid}, layer: ${e.layerindex}`, key: "layerclick", duration: 5 } e && message.info(msg); }) curDoc = doc; } // 先獲取模板信息 tplInfo = await getTemplateInfo(templateSectId, templateSecVersion); // 隨機生成一個剖面圖 creatSectDataMap(); // UI界面 const App = () => { return ( <div> <div className="info" style={{width: '430px'}}> <div className="input-item"> <button className="btn btn-full mr0" onClick={creatSectDataMap}>隨機生成一個剖面圖[前端直接繪製,適合於生成圖不大的情況]</button> <button className="btn btn-full mr0" onClick={creatSectDwgMap}>隨機生成剖面圖[後臺生成DWG前端展示,適合於生成圖大的情況]</button> <button className="btn btn-full mr0" onClick={exportDwgOpen}>導出成DWG圖並打開</button> </div> </div> </div> ); } ReactDOM.render(<App />, document.getElementById('ui')); const mousePositionControl = new vjmap.MousePositionControl(); map.addControl(mousePositionControl, "bottom-left");