前言
用資料生成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) 根據唯杰地圖https://vjmap.com/ SDK中提供創建CAD物體型別的方法創建相關物體, 唯杰地圖SDK支持的物體型別有DbLine直線、DbCurve曲線、Db2dPolyline二維折線、Db3dPolyline三維多段線、DbPolyline多段線、BlockReference塊參照、DbArc圓弧、DbCircle圓、DbEllipse橢圓、DbHatch填充、Text單行文本、DbMText多行文本、RasterImage柵格圖片、DbShape型物體、Spline樣條曲線、Wipeout遮罩物體、Dimension標注、Db2LineAngularDimension角度標注[兩條線]、Db3PointAngularDimension角度標注[三點]、DbAlignedDimension對齊標注、DbArcDimension圓弧標注、DbDiametricDimension直徑標注、DbOrdinateDimension坐標標注、DbRadialDimension半徑標注、DbRadialDimensionLarge半徑折線標注、DbRotatedDimension轉角標注、AcDbAttributeDefinition屬性注記、AcDbAttribute塊屬性、DbLayer圖層、DbTextStyle文字樣式、DbDimStyle標注樣式、DbLinetypeStyle線型樣式、DbBlock塊定義、DbDocument資料庫檔案,
如何減少代碼量可以用如下方法:
-
技巧一:可以直接拷貝模板中的物體,對物體的屬性進行修改,這樣能少賦值引數,減少代碼量,
-
技巧二:對于重復的物件,可以創建塊,變化的文字,以塊屬性文字定義,再重復創建塊參照,修改屬性文字,
(4) 把創建的資料生成一個JSON物件,呼叫唯杰地圖服務,后臺創建DWG圖形,
(5) 把后臺創建的DWG圖形資料以GeoJson資料或GIS瓦片的格式回傳給前端進行展示,對于圖不大的情況,可用GeoJson資料進行展示,如果圖大時,GeoJson資料量大,資料回傳慢,渲染也會受影響,這時建議用GIS柵格瓦片或矢量瓦片的時候進行繪制,
在線體驗地址
https://vjmap.com/demo/#/demo/map/comprehensive/03datatodwgmap
應用場景
能在前端通過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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/(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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/() => {
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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/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 = https://www.cnblogs.com/vjmap/archive/2023/03/05/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");
