唯杰地图 logo唯杰地图
VJMAP
EN

ventilation-three-views

vjcad/references/autodraw/examples/ventilation-three-views.md 34.1 KB761 行
原文

本文回答:壁挂式通风过滤装置 CAD 三视图(主视、左视、俯视)按用户给定绝对坐标绘制;展示自动成图通用骨架在大型工程图中的伸缩性——同样是 autodraw/overview 五步走,只是图层多 2 个、实体多 130+、标注链长一些。重点示范:① 视图间距用绝对坐标分块;② 隐藏 / 中实 / 中心 / 粗实四种线型混排;③ 标注层叠的 3 列分布避免堆成一墙;④ MTextEnt 多行文字 + 视图名 / 风机细节 / dimstyle 三件套让"几何图"升级到"可交付图"(详第八节)。

一、需求拆解

通风过滤装置 CAD 三视图绘制指令(含完整标注与开口)
单位:mm,绘图坐标比例 1:1
线型:外轮廓粗实线,开口/百叶中实线,中心线点划线,隐藏结构虚线
视图排布:三视图间距 ≥1500 mm,完全分离不重叠

部件 主视 左视 俯视 图层
箱体外框 ✓ ✓ ✓ OUTLINE
滤芯口(百叶 + 法兰) 3 个正面 3 个左侧 各面投影 OPENING
顶部出风口 Φ600/Φ700 ✓ – ✓ OPENING
风机外形 + 内部细节(电机 + 4 辐条 + 4 角加强筋 + "风机"标签) ✓ – – OUTLINE / TEXT
风机底座投影 – – ✓ OPENING
背部支架 – ✓(虚线) – HIDDEN
底面进风口 – ✓(虚线) ✓(虚线) HIDDEN
中心线 ✓ ✓ ✓ CENTER
标注 ~12 ~6 ~8 DIM
视图标签(主视图 / 左视图 / 俯视图 1:100) ✓ ✓ ✓ TEXT
技术说明(MText) + "比例 1:100" 题块 – – 右下角 TEXT

总实体 ≈ 130。

二、视图坐标布局

俯视 [0,4000]──[2000,5200]
         │
         │ 间距 1500
         │
主视 [0,0]────[2000,3450]            左视 [4000,0]──[5200,2500]    技术说明 [6200,2400]→[9200,~1230]
         └─────── 间距 2000 ───────────┘                            └─ width 3000 / textAttachment=7 ─┘
                                                                    比例题块 [6200,800]→[9200,1100]
  • 主视右边界 X=2000 → 左视左边界 X=4000,水平间距 2000 mm(≥1500 ✓)
  • 主视上边界 Y=3450 → 俯视下边界 Y=4000(用风机区算到 Y=2500 时间距 1500,含风机时仍 ≥ 550,本例选 Y=4000 即从主视上边距 550 到俯视下边)
  • 左视右边界 X=5200 → 技术说明 MText 左边界 X=6200,水平间距 1000 mm(避免 MText 锚点漂移盖到左视图)
  • 注:用户给的 "三视图间距 ≥ 1500" 指箱体投影间距——主视箱体顶 2500 → 俯视底 4000 = 1500 ✓

三、生成步骤

flowchart LR
    L["1.ensureLayers<br/>OUTLINE/OPENING/HIDDEN<br/>CENTER/DIM/TEXT"]
    L --> D["2.setupDimStyle<br/>DIMSCALE 50 → 1:100 可读"]
    D --> F["3.主视 buildFrontView<br/>(箱体+3 百叶+顶部出风口<br/>+风机[电机+支架+'风机'标签])"]
    F --> S["4.左视 buildLeftView<br/>(箱体+3 百叶+背支架+底进风口虚线)"]
    S --> T["5.俯视 buildTopView<br/>(投影外框+各面投影+顶出风口+风机底座)"]
    T --> V["6.buildViewLabels<br/>主视图 / 左视图 / 俯视图"]
    V --> N["7.技术说明 buildTitleNote<br/>(MText 高 150+'比例 1:100' 题块)"]
    N --> C["8.commit<br/>addEntities+regen+zoomExtents"]

四、完整可运行代码

4.1 顶层 + 通用工具

import {
  Engine, PolylineEnt, LineEnt, CircleEnt, TextEnt, MTextEnt,
  LinearDimensionEnt, DiametricDimensionEnt, Point2D,
  writeMessage
} from 'vjcad';

await cadView.onLoad();

drawVentilation();

function drawVentilation() {
  Engine.startUndoRecord('VENTILATION_3_VIEWS');
  try {
    ensureVentLayers();
    setupDimStyle();                       // 让默认标注在 1:100 出图时仍可读
    const ents = [];
    ents.push(...buildFrontView());
    ents.push(...buildLeftView());
    ents.push(...buildTopView());
    ents.push(...buildViewLabels());       // 主视图 / 左视图 / 俯视图
    ents.push(...buildTitleNote());
    Engine.addEntities(ents);
    writeMessage(`通风装置三视图共 ${ents.length} 个实体`);
  } finally {
    Engine.endUndoRecord();
  }
  Engine.regen();
  Engine.zoomExtents();
}

function setupDimStyle() {
  // 代码 new 出来的标注默认用全局 DefaultDimensionStyle 的副本,改文档样式对象(dimensionStyles.itemByName)不生效;
  // 文档比例走 DIMSCALE,每个标注再调 applyCurrentDocStyle() 吸取(见 hDim / vDim)。
  Engine.currentDoc.DIMSCALE = 50;   // 整体放大 50× → 1:100 出图时字高 ≈ 2.5 mm/纸面
}

function ensureVentLayers() {
  const defs = [
    { name: 'OUTLINE', color: 7, lineType: 'CONTINUOUS' },
    { name: 'OPENING', color: 3, lineType: 'CONTINUOUS' },
    { name: 'HIDDEN',  color: 2, lineType: 'HIDDEN'     },
    { name: 'CENTER',  color: 4, lineType: 'CENTER'     },
    { name: 'DIM',     color: 1, lineType: 'CONTINUOUS' },
    { name: 'TEXT',    color: 7, lineType: 'CONTINUOUS' }
  ];
  for (const d of defs) {
    if (!Engine.currentDoc.layers.itemByName(d.name)) {
      Engine.createLayer(d.name, d);
    }
  }
}

function rectClosed(p1, p2) {
  const pl = new PolylineEnt();
  pl.setPoints([[p1[0], p1[1]], [p2[0], p1[1]], [p2[0], p2[1]], [p1[0], p2[1]]]);
  pl.isClosed = true;
  pl.setDefaults();
  return pl;
}

function crossDiagonals(p1, p2) {
  const a = new LineEnt(p1, p2);
  const b = new LineEnt([p1[0], p2[1]], [p2[0], p1[1]]);
  a.setDefaults(); b.setDefaults();
  return [a, b];
}

function centerCross(cx, cy, halfH, halfV) {
  const a = new LineEnt([cx - halfH, cy], [cx + halfH, cy]);
  const b = new LineEnt([cx, cy - halfV], [cx, cy + halfV]);
  a.setDefaults(); b.setDefaults();
  return [a, b];
}

function hDim(p1, p2, textPosY) {
  const d = new LinearDimensionEnt(
    new Point2D(p1[0], p1[1]), new Point2D(p2[0], p2[1]),
    new Point2D((p1[0] + p2[0]) / 2, textPosY), 0);
  d.setDefaults();
  d.applyCurrentDocStyle();   // 吸取文档当前标注样式,scaleFactor 取自 DIMSCALE
  return d;
}

function vDim(p1, p2, textPosX) {
  const d = new LinearDimensionEnt(
    new Point2D(p1[0], p1[1]), new Point2D(p2[0], p2[1]),
    new Point2D(textPosX, (p1[1] + p2[1]) / 2), Math.PI / 2);
  d.setDefaults();
  d.applyCurrentDocStyle();
  return d;
}

4.2 主视图

function buildFrontView() {
  const ents = [];

  Engine.setCurrentLayer('OUTLINE');
  Engine.CECOLOR = 256;
  Engine.CELTYPE = 'ByLayer';
  ents.push(rectClosed([0, 0], [2000, 2500]));      // 箱体
  ents.push(...buildFan({ x0: 500, y0: 2500, x1: 1500, y1: 3450 }));

  Engine.setCurrentLayer('OPENING');
  const frontOpens = [
    [[664, 192],  [1336, 864]],
    [[664, 914],  [1336, 1586]],
    [[664, 1636], [1336, 2308]]
  ];
  for (const [p1, p2] of frontOpens) {
    ents.push(rectClosed(p1, p2));
    ents.push(...crossDiagonals(p1, p2));
  }

  const ventOuter = new CircleEnt([1000, 2500], 350);
  const ventInner = new CircleEnt([1000, 2500], 300);
  ventOuter.setDefaults();  ventInner.setDefaults();
  ents.push(ventOuter, ventInner);

  Engine.setCurrentLayer('CENTER');
  ents.push(...centerCross(1000, 2500, 400, 400));

  ents.push(...buildFrontDims());
  return ents;
}

function buildFan({ x0, y0, x1, y1 }) {
  const out = [];
  Engine.setCurrentLayer('OUTLINE');
  out.push(rectClosed([x0, y0], [x1, y1]));

  const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
  const motor = new CircleEnt([cx, cy], 200);
  const hub   = new CircleEnt([cx, cy], 60);
  motor.setDefaults();  hub.setDefaults();
  out.push(motor, hub);

  for (let i = 0; i < 4; i++) {
    const a = Math.PI / 4 + i * Math.PI / 2;
    const ln = new LineEnt(
      [cx + 60 * Math.cos(a),  cy + 60 * Math.sin(a)],
      [cx + 200 * Math.cos(a), cy + 200 * Math.sin(a)]
    );
    ln.setDefaults();
    out.push(ln);
  }

  const corners = [
    [[x0, y0], [x0 + 80, y0 + 80]],
    [[x1, y0], [x1 - 80, y0 + 80]],
    [[x0, y1], [x0 + 80, y1 - 80]],
    [[x1, y1], [x1 - 80, y1 - 80]]
  ];
  for (const [a, b] of corners) {
    const ln = new LineEnt(a, b);
    ln.setDefaults();
    out.push(ln);
  }

  Engine.setCurrentLayer('TEXT');
  const label = new TextEnt();
  label.insertionPoint = [x0 + 220, cy - 75];
  label.text   = '风机';
  label.height = 150;
  label.styleName = 'simkai';
  label.setDefaults();
  out.push(label);

  Engine.setCurrentLayer('OUTLINE');
  return out;
}

function buildFrontDims() {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([0, 0], [2000, 0], -120));            // 箱体宽 2000
  out.push(hDim([500, 3450], [1500, 3450], 3580));    // 风机宽 1000
  out.push(hDim([664, 0], [1336, 0], -240));          // 百叶宽 672
  out.push(hDim([0, 0], [664, 0], -360));             // 左侧空 664
  out.push(hDim([1336, 0], [2000, 0], -360));         // 右侧空 664

  const cInner = 2150, cMid = 2350, cOuter = 2600;
  out.push(vDim([2000, 0],    [2000, 192],  cInner));   // 风口下空 192
  out.push(vDim([2000, 864],  [2000, 914],  cInner));   // 间距 50
  out.push(vDim([2000, 1586], [2000, 1636], cInner));   // 间距 50
  out.push(vDim([2000, 2308], [2000, 2500], cInner));   // 风口上空 192

  out.push(vDim([2000, 192],  [2000, 864],  cMid));     // 第 1 风口 672
  out.push(vDim([2000, 914],  [2000, 1586], cMid));     // 第 2 风口 672
  out.push(vDim([2000, 1636], [2000, 2308], cMid));     // 第 3 风口 672

  out.push(vDim([2000, 0],    [2000, 2500], cOuter));         // 箱体高 2500
  out.push(vDim([2000, 0],    [2000, 3450], cOuter + 200));   // 总高 3450
  out.push(vDim([1500, 2500], [1500, 3450], 1700));           // 风机高 950

  const dimD600 = new DiametricDimensionEnt(
    new Point2D(700, 2500), new Point2D(1300, 2500));
  const dimD700 = new DiametricDimensionEnt(
    new Point2D(650, 2500), new Point2D(1350, 2500));
  dimD600.setDefaults();  dimD700.setDefaults();
  dimD600.applyCurrentDocStyle();  dimD700.applyCurrentDocStyle();
  out.push(dimD600, dimD700);

  return out;
}

4.3 左视图

function buildLeftView() {
  const ents = [];
  const ox = 4000;

  Engine.setCurrentLayer('OUTLINE');
  ents.push(rectClosed([ox, 0], [ox + 1200, 2500]));

  Engine.setCurrentLayer('OPENING');
  const leftOpens = [
    [[ox + 80, 192],  [ox + 752, 864]],
    [[ox + 80, 914],  [ox + 752, 1586]],
    [[ox + 80, 1636], [ox + 752, 2308]]
  ];
  for (const [p1, p2] of leftOpens) {
    ents.push(rectClosed(p1, p2));
    ents.push(...crossDiagonals(p1, p2));
  }

  Engine.setCurrentLayer('HIDDEN');
  ents.push(rectClosed([ox + 1200, 1150], [ox + 1700, 1350])); // 背支架 500×200
  ents.push(rectClosed([ox + 80, -50],   [ox + 752,  0]));     // 底面进风口
  ents.push(...crossDiagonals([ox + 80, -50], [ox + 752, 0]));

  ents.push(...buildLeftDims(ox));
  return ents;
}

function buildLeftDims(ox) {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([ox, 0], [ox + 1200, 0], -120));            // 深 1200
  out.push(hDim([ox + 80, 0], [ox + 752, 0], -240));        // 百叶 672

  const cx = ox + 1200 + 200;
  out.push(vDim([ox + 1200, 192],  [ox + 1200, 864],  cx));
  out.push(vDim([ox + 1200, 914],  [ox + 1200, 1586], cx));
  out.push(vDim([ox + 1200, 1636], [ox + 1200, 2308], cx));
  out.push(vDim([ox + 1200, 0],    [ox + 1200, 2500], cx + 200));

  return out;
}

4.4 俯视图

function buildTopView() {
  const ents = [];
  const oy = 4000;

  Engine.setCurrentLayer('OUTLINE');
  ents.push(rectClosed([0, oy], [2000, oy + 1200]));

  const ventOuter = new CircleEnt([1000, oy + 600], 350);
  const ventInner = new CircleEnt([1000, oy + 600], 300);
  ventOuter.setDefaults();  ventInner.setDefaults();
  ents.push(ventOuter, ventInner);

  Engine.setCurrentLayer('CENTER');
  ents.push(...centerCross(1000, oy + 600, 400, 400));

  Engine.setCurrentLayer('OPENING');
  ents.push(rectClosed([500, oy + 200], [1500, oy + 1000]));   // 风机底座

  const frontProj = [[664, oy], [1336, oy + 672]];
  ents.push(rectClosed(frontProj[0], frontProj[1]));

  const leftProj  = [[0, oy + 208], [672, oy + 880]];
  ents.push(rectClosed(leftProj[0], leftProj[1]));

  const rightProj = [[1328, oy + 208], [2000, oy + 880]];
  ents.push(rectClosed(rightProj[0], rightProj[1]));

  Engine.setCurrentLayer('HIDDEN');
  const bottomHidden = [[664, oy + 672], [1336, oy + 1344]];   // 用户给值
  ents.push(rectClosed(bottomHidden[0], bottomHidden[1]));
  ents.push(...crossDiagonals(bottomHidden[0], bottomHidden[1]));

  ents.push(...buildTopDims(oy));
  return ents;
}

function buildTopDims(oy) {
  Engine.setCurrentLayer('DIM');
  const out = [];

  out.push(hDim([0, oy + 1200], [2000, oy + 1200], oy + 1320));   // 宽 2000
  out.push(hDim([0, oy], [2000, oy], oy - 120));                  // 宽 2000 复用一条
  out.push(vDim([0, oy], [0, oy + 1200], -120));                  // 深 1200

  const dimVent = new DiametricDimensionEnt(
    new Point2D(700, oy + 600), new Point2D(1300, oy + 600));
  dimVent.setDefaults();
  dimVent.applyCurrentDocStyle();
  out.push(dimVent);

  out.push(hDim([664, oy], [1336, oy], oy - 240));                // 正面投影宽 672
  out.push(vDim([0, oy + 208], [0, oy + 880], -240));             // 左 / 右投影深 672
  out.push(vDim([2000, oy + 208], [2000, oy + 880], 2120));

  return out;
}

4.5 视图标签(在三视图下方加 "主视图 / 左视图 / 俯视图")

function buildViewLabels() {
  Engine.setCurrentLayer('TEXT');
  const out = [];

  const labels = [
    { text: '主视图  1:100', insertion: [600,  -350], textAlignment: 7 },
    { text: '左视图  1:100', insertion: [4200, -350], textAlignment: 7 },
    { text: '俯视图  1:100', insertion: [600,  5400], textAlignment: 1 }
  ];
  for (const { text, insertion,textAlignment } of labels) {
    const t = new TextEnt();
    t.insertionPoint = insertion;
    t.text = text;
    t.textAlignment = textAlignment;
    t.height = 200;
    t.styleName = 'simkai';
    t.setDefaults();
    out.push(t);
  }
  return out;
}

视图名称用单行 TextEnt(高 200 mm),放在每个视图下方留白处;中文字体 simkai 必须先在 MainView({ fonts }) 注册(详 setup/fonts)。

4.6 技术说明(MText)+ 比例题块

function buildTitleNote() {
  Engine.setCurrentLayer('TEXT');
  const out = [];

  const noteText =
    '壁挂式通风过滤装置,设计风量 10000m³/h,过滤等级 G4。\n' +
    '箱体 2000×1200×2500mm,1.5mm 镀锌钢板喷塑。\n' +
    '前、左、右立面各 3 个滤芯口,底面 1 个进风口,配固定百叶窗。\n' +
    '顶部 DN600 出风口,风机柔性软接。\n' +
    '前方检修空间 ≥800mm,两侧及顶部空间 ≥500mm,墙体承重 ≥350kg。\n' +
    '符合 GB/T 14295、GB 50243 标准。';

  const mt = new MTextEnt();
  mt.insertionPoint  = [6200, 2400];   // ⬅ 由 5800 挪到 6200:再加 400 mm 安全间距
  mt.textAttachment  = 7;              // ⬅ 7 = TopLeft(小键盘布局):插入点 = 文本框左上角,向右下生长
  mt.text   = noteText;
  mt.height = 150;                     // 1:100 出图后纸面 1.5 mm 仍可读
  mt.maxWidth = 3000;                  // MTextEnt 无 width 字段;maxWidth=文本框最大宽度(refWidth 等价别名)
  mt.styleName = 'simkai';
  mt.setDefaults();
  out.push(mt);

  const scaleBox = new PolylineEnt();
  scaleBox.setPoints([[6200, 800], [9200, 800], [9200, 1100], [6200, 1100]]);
  scaleBox.isClosed = true;
  scaleBox.setDefaults();
  out.push(scaleBox);

  const scale = new TextEnt();
  scale.insertionPoint = [6500, 880];
  scale.text   = '比例  1 : 100';
  scale.height = 200;                  // 由 80 提到 200:明显成"题块"
  scale.styleName = 'simkai';
  scale.setDefaults();
  out.push(scale);

  return out;
}

关键:MTextEnt.textAttachment 默认就是 7(MTextAttachmentEnum.TopLeft,插入点 = 文本框左上角,文字向右下生长)。vjcad 附着点按小键盘布局编号(7/8/9 = 上排,1/2/3 = 下排),不同于 DXF 的 1=TopLeft——写成 1 是 BottomLeft,文字会以插入点为左下角向上生长。这里仍显式赋值——一是代码可读(读者不用猜锚点),二是防御未来默认值变更。若误设成 5(MidCenter)会以插入点为中心向四周溢出,盖到临近视图。详见 entity/text-mtext 第三节。

五、关键技术点

5.1 视图分块:绝对坐标 vs 偏移变换

用户给了所有视图的绝对坐标——本例直接用绝对坐标分块(每个 buildXxxView 内部所有点都是绝对值),最简单、便于调试 / 量距。

若同样的"通风装置"要画 1:5 缩小版,把所有坐标乘 0.2 即可;或先在原点构造,最后整组用 entity.move([0,0],[ox,oy]) 平移(详 edit/move-copy)。

5.2 数据驱动 vs 硬编码

3 个百叶用 frontOpens 数组 + for 循环替代手写 3 次:

const frontOpens = [
  [[664, 192],  [1336, 864]],
  [[664, 914],  [1336, 1586]],
  [[664, 1636], [1336, 2308]]
];
for (const [p1, p2] of frontOpens) {
  ents.push(rectClosed(p1, p2));
  ents.push(...crossDiagonals(p1, p2));
}

好处:① 改间距只动数据,不动代码;② 与"5 个 / 7 个百叶"扩展只改数组;③ 代码更短易审。

5.3 标注分列(避免链尺寸堆成一堵墙)

主视图右侧 7 段竖向尺寸如果都堆同一 textPosX,渲染后会变成"一堵密不透风的红墙",看不清单条尺寸。本例改成3 列错开:

                  cInner    cMid     cOuter
y=2500 ─┬──────── ┐         ┬───── ┬─── ┐
        │   192   │         │ 2500 │3450│
y=2308 ─┴┐        │         │      │    │
         │   672  │ ←第3口  │      │    │
y=1636 ──┘        │         │      │    │
y=1586 ─┬─        │         │      │    │
        │    50   │ ←间距   │      │    │
y=1636 ─┴─        │         │      │    │
        │   672   │ ←第2口  │      │    │
y=914  ──         │         │      │    │
...
列 textPosX 谁去
cInner=2150(内) 4 段小缝(上下空 192 / 中间间距 50)
cMid=2350(中) 3 段风口主尺寸 672
cOuter=2600 / 2800(外) 箱高 2500、总高 3450

step ≥ 字高 × 2 防文字重叠(详 autodraw/overview 第四节 4.5)。

5.4 隐藏 / 中实 / 中心 / 粗实 四线型

线型靠图层 + setDefaults() 自动应用:

线型 图层 lineType 字段 颜色
粗实 OUTLINE CONTINUOUS 7(白 / 黑)
中实 OPENING CONTINUOUS 3(绿)
虚线 HIDDEN HIDDEN 2(黄)
点划 CENTER CENTER 4(青)

前提:项目模板里已加载这些线型;vjcad 默认带 CONTINUOUS / HIDDEN / CENTER,自定义线型见 property/color-linetype。

"粗实 / 中实"在 vjcad 里用图层 ACI 颜色 + ACI 线宽体现;想要严格的 lineWeight 区分(如打印时 0.7 / 0.35 mm),在创建后设图层字段:const ly = Engine.createLayer('OUTLINE', { color: 7 }); ly.lineWeight = 70;——createLayer 的 options 只认 layerOn / color / lineType / isFrozen / isLocked,传 lineWeight 会被静默丢弃。

5.5 中文字体 MText / TextEnt

MTextEnt.styleName = 'simkai' 必须配合 MainView({ fonts: [{ name: 'simkai', path: '...' }] }) 在初始化时注册(详 setup/fonts、entity/text-mtext)。否则中文显示成方框 / 缺字。

字高与出图比例匹配(1:100 出图,几何字高建议 ≥ 100 mm,对应纸面 1 mm 起):

用途 实体 几何字高 1:100 纸面
标题 / 视图名 TextEnt 200 mm 2 mm
比例题块 TextEnt 200 mm 2 mm
部件标签(风机) TextEnt 150 mm 1.5 mm
多行技术说明 MTextEnt 150 mm 1.5 mm
标注文字 dimstyle textHeight × scaleFactor 2.5×50=125 mm 见 8.1

MTextEnt.maxWidth = 3000 决定换行宽度(属性名是 maxWidth,不是 width;curWidth 为只读实际宽度);多行总高 ≈ height × 行数 × 1.2 ~ 1.5,6 行 × 150 ≈ 1170 mm。

textAttachment 建议显式设:默认虽已是 7(TopLeft),但多行说明文字块建议写死,避免复制来的代码段带其它锚点值,见红旗 #14:

mt.textAttachment = 7;   // 小键盘布局:7=TopLeft / 8=TopCenter / 9=TopRight / 4=MidLeft / 5=MidCenter / 6=MidRight / 1=BottomLeft / 2=BottomCenter / 3=BottomRight

5.6 buildFan / buildViewLabels / setupDimStyle 三个"成品图"补丁

为把"几何图"升级到"可交付图",本例增加三个独立小函数(详第八节):

函数 解决
buildFan 风机不再是空白矩形,加电机 / 4 辐条 / 4 角加强筋 / "风机"标签
buildViewLabels 三个视图下方加"主视图 / 左视图 / 俯视图 1:100" TextEnt
setupDimStyle 设 Engine.currentDoc.DIMSCALE = 50,配合每个标注 applyCurrentDocStyle(),让 1:100 出图后字高 ≈ 2.5 mm/纸面

5.7 crossDiagonals 的 4 点顺序

每个百叶画两条交叉对角线 = LineEnt(p1, p2) + LineEnt([p1.x, p2.y], [p2.x, p1.y])——两条线在矩形对角点对调 y 即可。这种"两点矩形对角对调"是图层 OPENING 的常见模式。

六、标注布局规则

主视右侧的纵向链尺寸分三列而非堆一列,避免文字 / 尺寸线打架:

列 textPosX 用途 数量
内层(gap 列) cInner = 2150 上下空 192 + 间距 50 4
中层(开口列) cMid = 2350 3 个 672 风口 3
外层(总尺寸列) cOuter = 2600 / 2800 箱体高 2500、总高 3450 2

横向 / 三视图整体规则:

视图 横向标注 textPosY 纵向标注 textPosX step
主视 上方 y=3580、下方 y=-120 / -240 / -360 cInner / cMid / cOuter 三列 120
左视 下方 y=-120 / -240 右侧 x=ox+1400、x=ox+1600 120
俯视 上方 y=oy+1320、下方 y=oy-120 / oy-240 左侧 x=-120 / -240、右侧 x=2120 120

字高默认 50(DIMSCALE = 50 + 每个标注 applyCurrentDocStyle(),让 1:100 出图后纸面字高 ≈ 2.5 mm);step = 120 ≈ 字高 × 2.4 防止文字与下一条尺寸线压叠。

七、红旗 / 反例

  1. 左视图忘了加 ox = 4000 → 整组左视图盖在主视图上,几何相互压死;先 console.log 几个左视图实体的 bbox 看看是否在 X >= 4000。
  2. 百叶画在 OUTLINE 层 → 与箱体外框同色 / 同线型,看不出"开口"语义;必须 setCurrentLayer('OPENING') 后再画百叶 + 对角线。
  3. HIDDEN 线显示成实线 → 模板没加载 HIDDEN 线型;修:Engine.linetypeManager.registerLinetype(def) 或 loadFromLinDefinitions(lin文本) 加载(小写 t,详 trouble/source-conflicts §7;无 .add())(详 property/color-linetype),或先用 Engine.regen(true) 强刷一次。
  4. MTextEnt 没设 styleName='simkai' → 中文渲染成方框;先在 MainView({ fonts: [...] }) 注册中文字体。
  5. textPosX / textPosY 写错让标注盖在几何上 → 调试时把 textPos 加 100 mm 移开看清,再回填精确值。
  6. 直径标注 DiametricDimensionEnt(中心, 圆周点) → vjcad 的直径标注不是圆心+半径而是直径两端点;本例外圆 Φ700:new Point2D(650, 2500) / new Point2D(1350, 2500),差 700 = Φ700。
  7. crossDiagonals 写成 LineEnt(p1, p2) + LineEnt(p1, p2):复制粘贴漏改 → 两条线重合,看不到 X 形;第二条要把 y 对调:[p1.x, p2.y] ↔ [p2.x, p1.y]。
  8. for (let i = 0; i < frontOpens.length; i++) 内手动 setCurrentLayer 重复切层:性能略差且容易写错;进入循环前切一次层就好。
  9. 底面进风口 Y 范围 [oy+672, oy+1344] 超出箱体 Y 范围 [oy, oy+1200]:用户原文如此,本例如实绘制;项目里真实交付前需要再核对一次。
  10. 链尺寸全堆同一 textPosX:渲染后视觉上挤成"一堵墙",文字相互压叠;本例用 3 列 cInner / cMid / cOuter 错开(详第六节)。
  11. MText height = 60 + 1:100 出图:纸面字高 0.6 mm,几乎看不见;本例改用 height = 150(纸面 1.5 mm);视图标签 TextEnt.height = 200(纸面 2 mm)。
  12. 风机就一个空白矩形:浪费 950 mm 高的占位;至少加电机圆 + 4 条安装支架短斜线 + "风机"文字标签(详 buildFan 函数)。
  13. 没视图名 / 没比例题块:图纸缺信息无法交付;本例增加 buildViewLabels 三个 TextEnt,比例标用单独的 PolylineEnt 闭合矩形 + 高 200 的 TextEnt。
  14. MText 误用居中锚点(textAttachment = 5 等) → 文字以插入点为中心向四周溢出,宽文本框会横向跨到临近视图上(默认 7 TopLeft 无此问题;误写 DXF 习惯的 1 则是 BottomLeft,文字向上长)。修法:mt.textAttachment = 7 显式写死 + 插入点放在"左视图右边界 + 1000 mm"以右,双保险。

八、提升出图质量的若干技巧

这一节回答"为什么默认渲染看起来一般,怎么改"——也是把本示例从"几何对了"升级到"成品图能交付"的关键。

8.1 标注样式 dimstyle

vjcad 默认 DimensionStyle.textHeight = 2.5 是按 AutoCAD 习惯(纸面单位)。1:1 大几何 + 1:100 出图时纸面字高变成 0.025 mm,等于隐形:

function setupDimStyle() {
  Engine.currentDoc.DIMSCALE = 50;   // 文档标注比例
}
// 每个代码创建的标注:
dim.setDefaults();
dim.applyCurrentDocStyle();          // 克隆文档当前样式,并把 scaleFactor 覆盖为 DIMSCALE
// 或逐个设快捷属性:dim.scaleFactor = 50 / dim.textHeight / dim.arrowSize

⚠️ 只改 dimensionStyles.itemByName('Standard').scaleFactor 无效:标注构造时 _style = DefaultDimensionStyle.clone()(全局默认样式,不是文档样式),setDefaults() 也不吸文档样式;applyCurrentDocStyle() 会吸文档样式但随即把 scaleFactor 覆盖成 doc.DIMSCALE。内置标注命令就是 setDefaults(); applyCurrentDocStyle(); 两步。

DimensionStyle 是驼峰字段名(scaleFactor / textHeight / arrowSize / extensionLineOffset 等);dimscale / dimtxt 这类小写名只出现在 DbDimensionStyle 序列化层,不能直接对样式对象赋值(详 entity/dimension-mleader 第四节"标注样式")。

8.2 视图名标签

每个视图下方放一行 TextEnt,高度 ≥ 字高 × 1.5,至少 200 mm:

const t = new TextEnt();
t.insertionPoint = [600, -350];
t.text   = '主视图  1:100';
t.height = 200;
t.styleName = 'simkai';
t.setDefaults();

视图名下还可加图框线(PolylineEnt 闭合矩形)做"题块"。

8.3 风机 / 风口等"占位部件"加细节

不要画一个空白矩形当风机——观图者会怀疑是图层背景。至少加:

元素 实体 数量
电机外圆 CircleEnt 1
电机轴轮毂 CircleEnt 1
4 条电机支架(45° 辐条) LineEnt 4
4 个角加强筋(角内 80×80 短线) LineEnt 4
"风机" 文字标签 TextEnt(高 150) 1
const motor = new CircleEnt([cx, cy], 200);
const hub   = new CircleEnt([cx, cy], 60);
for (let i = 0; i < 4; i++) {
  const a = Math.PI / 4 + i * Math.PI / 2;
  out.push(new LineEnt(
    [cx + 60 * Math.cos(a),  cy + 60 * Math.sin(a)],
    [cx + 200 * Math.cos(a), cy + 200 * Math.sin(a)]));
}

8.4 比例题块单独画 + MText 锚点

不要把"比例 1:100"塞在 MText 末行——观图者扫不到。给比例单独画一个题块:

const scaleBox = new PolylineEnt();
scaleBox.setPoints([[6200, 800], [9200, 800], [9200, 1100], [6200, 1100]]);
scaleBox.isClosed = true;
scaleBox.setDefaults();

const scale = new TextEnt();
scale.insertionPoint = [6500, 880];
scale.text   = '比例  1 : 100';
scale.height = 200;
scale.setDefaults();

MText 一定要设 textAttachment:

const mt = new MTextEnt();
mt.insertionPoint  = [6200, 2400];
mt.textAttachment  = 7;             // 小键盘布局:7=TopLeft / 8=TopCenter / 9=TopRight / 4~6=Mid* / 1=BottomLeft ... 3=BottomRight
mt.text   = '...';
mt.maxWidth = 3000;                 // 无 width 字段;maxWidth=文本框最大宽度
mt.height = 150;
mt.setDefaults();
锚点值 含义 插入点 = 文本生长方向
7 TopLeft(默认) 文本框左上角 向右下 ✓ 推荐
8 TopCenter 顶边中心 向左右 / 向下
9 TopRight 文本框右上角 向左下
5 MidCenter 文本框中心 向四周溢出
1 BottomLeft 文本框左下角 向右上

vjcad 默认值已是 7(TopLeft):显式赋值是防御性写法;注意 vjcad 按小键盘布局编号(MTextAttachmentEnum,与 TextEnt.textAlignment 同一布局),不是 DXF 的 1=TopLeft;真正危险的是误用 5(MidCenter)等居中锚点——文字会以插入点为中心向四周溢出,把临近视图盖住。见红旗 #14(详 entity/text-mtext 第三节)。

8.5 隐藏 / 中心 / 中实四线型在小屏 / 缩略图里很接近

为防"颜色相近、线型微差"在缩略图下糊成一团,靠图层颜色拉开主次:

用途 推荐 ACI 理由
OUTLINE 7(主题色) 最显眼
OPENING 3(亮绿) 中度
HIDDEN 2(黄) 在深背景中清晰
CENTER 4(青) 与黄不撞
DIM 1(红) 与几何拉开

详 property/color-linetype。

8.6 用 bbox 自检视图相互不压叠

import { mergeBoundingBoxes } from 'vjcad';

function bboxOf(ents) {
  const boxes = ents.map(e => e.boundingBox()).filter(b => b && (b.maxX - b.minX) > 0 && (b.maxY - b.minY) > 0);
  // 注意:boundingBox() 返回 BoundingBox 实例(pt1/pt2/width/height/center/minPoint/maxPoint),也有 minX..maxY getter
  return mergeBoundingBoxes(boxes);
}
const bFront = bboxOf(buildFrontView());
const bLeft  = bboxOf(buildLeftView());
console.assert(bFront.maxX < bLeft.minX, '主视、左视水平不分离');

详 geometry/point-bounds。

8.7 关键尺寸 / 直径用 MLeaderEnt 引出

零散 Φ 标注混在 LinearDimension 阵列里很难一眼定位;改用 MLeaderEnt 把"DN15 / DN600"引到图外空白区:

const ml = new MLeaderEnt();
ml.contentType  = MLeaderContentType.MText;
ml.textContent  = 'DN600';
ml.textPosition = new Point2D(2000, 2700);
ml.textHeight   = 120;
ml.addLeaderLine(new Point2D(1000, 2500));
ml.setDefaults();

详 entity/dimension-mleader 第三节。

九、扩展挑战(本例不实现,留给读者)

想增 提示
图框 + 标题栏 用 BlockDefinition 定义 A1 图框块,InsertEnt 引入(详 entity/block-insert)
比例 1:100 真缩放 几何画 1:1,scale([0,0], 0.01) 缩到纸上,标注用 scaleFactor = 100 反算(详 edit/rotate-scale)
视图标记 / 剖切线 在主视上加 A-A 剖切箭头(SolidEnt 三角箭头 + LineEnt 剖切线 + TextEnt "A-A")
不同墙厚 hatch 在隐藏 / OPENING 矩形内加 HatchEnt + ANSI31(详 entity/hatch-solid)
DWG 导出 见 file/dwg-import-export

参考(官方文档 MCP)