上一章把触控笔的写入过程与多指视图操作分开后,首层机电平面图已经有了稳定的批注锚点,也有给排水、强电和文字批注等图层。但当几条管线在狭小区域相交,两个文字标签就可能遮住同一处阀门。用户把“给水阀门”拖到合适位置后,改变图层可见性或旋转图纸,文字又挤到一起;如果此时直接修改锚点,就会把排版问题错误地写进业务位置。

本篇沿用4200×2970的图纸页坐标和前文的变换矩阵。给水阀门的页锚点为(2600,1850),附近还要放一条“管道检修口”批注。要解决的是如何给这些标签选择不互相遮挡的文字框位置,以及相同输入反复计算时为何应得到稳定结果,而不是重新选择随机偏移。

文本批注布局与锚点避让流程

一、固定锚点与可移动文字框是两种不同的数据

批注结构中需要把anchorX、anchorY与labelX、labelY分开。前者属于图纸原始页面坐标,表达被标注对象在哪里;后者是文字框左上角位置,可以为了避让而改变。还要保存annotationId、layerId、text、priority、pinned、measuredWidth、measuredHeight和revision。原本的图层显隐字段保留在图层表,不应因为某个图层隐藏就把对应的标签删除。

绘制时先对锚点应用第二篇的页面到屏幕变换,再对文字框应用同一套变换。文字框移动后,需要画一段从锚点到框边的引导线,让阅读者知道标签仍然对应那个阀门。若在不同位置重复保存anchorX,就会出现同一批注随着缩放发生漂移的情况。业务存储里始终以页坐标为准,只有编辑器的短暂交互状态才使用屏幕触点。

界面还需要区分自动排列和人工固定。设计人员手动安置过的批注,可使用pinned=true参与障碍检测;自动排列时优先给这些框留位置。真正被固定的是文字框布局选择,不是原始锚点的存在方式。若固定框超出图纸边界,应明确提示重新定位,不能为了把所有标签塞进画面而覆盖人工选择。

二、文本测量要先于碰撞判断

两个标签能否共存,取决于文字真实占用的矩形,而不是字符个数。“给水阀门”与同样长度的英文字母、数字在不同字号下宽度不同,是否启用粗体以及断行规则也会影响高度。ArkGraphics 2D提供文本段落与测量相关能力,界面可取得布局后的宽高,再交给业务排布算法。绘制方案如果选择ArkUI的Text组件,也需要利用组件自身完成布局测量;不要把固定的“每字多少像素”当成所有字体的通用值。

为了避免当前缩放倍率变化导致每次重新排版,我们选取一个统一的页面布局尺度,把测量结果转换为页坐标单位。日常缩放只改变最终展示矩阵,而不是重新生成排列。字号或正文发生真正变化时,才以新的测量结果重算布局。用户主动切换显示密度,则可以另设排版配置版本并保存它,使下一次进入页面能够复现当前选择。

业务层接收的是经过文字引擎测量的宽高,并先做输入检查。宽高必须是有限正数,锚点必须在图纸界内,文本空值则不给自动排列。代码只处理已测量的矩形,不绑定具体的系统文本类名。

function normalizeLabel(input, pageWidth = 4200, pageHeight = 2970) {
  const required = [input.anchorX, input.anchorY, input.width, input.height];
  if (required.some(value => !Number.isFinite(value))) {
    throw new TypeError('LABEL_METRICS');
  }
  if (input.anchorX < 0 || input.anchorX > pageWidth ||
      input.anchorY < 0 || input.anchorY > pageHeight ||
      input.width <= 0 || input.height <= 0 || !String(input.text || '').trim()) {
    throw new RangeError('LABEL_OUT_OF_RANGE');
  }
  return { ...input, text: input.text.trim(),
    priority: Number.isFinite(input.priority) ? input.priority : 100 };
}

这一步的意义在于隔离异常测量结果。字体资源尚未准备好时,宽度可能为0;空文字也不应该占据一片碰撞区域。界面应保持原有批注并提示重新测量,不能把它们变成页面左上角的零尺寸矩形。测量输入也适合做缓存,缓存键应包含文本内容、字体、字号、字重和布局配置版本。

三、候选位置应当有限、可排序、可解释

对每个批注,先围绕锚点给出固定顺序的几个候选框:右上、右下、左上、左下,再加上必要的更远偏移。候选矩形要落在页面界内,并避开已经安置的标签以及设计上不能覆盖的图例区域。只有通过这些条件的候选位置才能选用。候选集合有限,能够控制计算开销,也能解释某条标签为什么被放在某一侧。

我把边界检查和相交判定放在一起。对于刚好相邻的两个矩形,接触边缘不计重叠;界面可以额外配置留白距离,让实际文字读起来不会挤在一起。下面的代码提供八个有序候选,返回的是文字框的位置而非新的原始锚点。

function boxesOverlap(a, b, gap = 0) {
  return !(a.x + a.w + gap <= b.x || b.x + b.w + gap <= a.x ||
    a.y + a.h + gap <= b.y || b.y + b.h + gap <= a.y);
}
function candidateBoxes(label, gap = 12) {
  const { anchorX: x, anchorY: y, width: w, height: h } = label;
  return [
    { x: x + gap, y: y - gap - h, w, h },
    { x: x + gap, y: y + gap, w, h },
    { x: x - gap - w, y: y - gap - h, w, h },
    { x: x - gap - w, y: y + gap, w, h },
    { x: x + 2 * gap, y: y - 2 * gap - h, w, h },
    { x: x + 2 * gap, y: y + 2 * gap, w, h },
    { x: x - 2 * gap - w, y: y - 2 * gap - h, w, h },
    { x: x - 2 * gap - w, y: y + 2 * gap, w, h }
  ];
}

固定顺序比临时随机生成坐标更容易排查。这里的gap采用统一的页坐标间距,使用屏幕像素输入时应先按当前约定的画布尺度换算。若图纸中存在禁止遮挡的尺寸线区域,也可以把其包围框加入reserved列表,作为相同的矩形障碍参与判断。对于不规则的管线轮廓,可以先用近似包围框筛选,再针对少量候选做更细致的路径检测。

四、重排时保持图层、优先级和结果稳定

如果不规定处理顺序,每次增删一条标签都可能把全页面的文字打乱。排布前把可见图层里的批注按pinned、priority和annotationId稳定排序:人工固定的优先,其次是重要程度,最后用不变的业务标识打破并列。隐藏图层的数据保持原位,不进入当前布局,却不能从存储中抹掉。原本已经安置且依然满足约束的文本框,可以放入首个候选,减少视觉跳动。

随后依次尝试候选矩形,满足页面边界、避让保留区和已选框不相交,就把结果写入排布计划;没有合适位置的标签进入unplaced,提示人工调整。不能为了制造“无冲突”结论直接丢弃该标签。图中的两个邻近标注因此可以放到阀门两侧,并用不同引导线连接,但两个页锚点仍在原始图纸位置。

function stableReflow(rawLabels, visibleLayerIds, reserved = [], page = { w: 4200, h: 2970 }) {
  const visible = new Set(visibleLayerIds);
  const labels = rawLabels.filter(x => visible.has(x.layerId))
    .map(x => normalizeLabel(x, page.w, page.h))
    .sort((a, b) => Number(b.pinned) - Number(a.pinned) ||
      a.priority - b.priority || a.id.localeCompare(b.id));
  const occupied = [...reserved];
  const placed = [];
  const unplaced = [];
  for (const label of labels) {
    const previous = label.pinned && label.labelBox ? [label.labelBox] : [];
    const options = [...previous, ...candidateBoxes(label)];
    const chosen = options.find(box => box.x >= 0 && box.y >= 0 &&
      box.x + box.w <= page.w && box.y + box.h <= page.h &&
      !occupied.some(other => boxesOverlap(box, other, 6)));
    if (!chosen) { unplaced.push(label.id); continue; }
    occupied.push(chosen);
    placed.push({ id: label.id, layerId: label.layerId,
      anchorX: label.anchorX, anchorY: label.anchorY, labelBox: chosen });
  }
  return { placed, unplaced };
}

对于人工固定标签,还应在编辑时判断选中框是否与其他固定框冲突;上面的回退候选用于处理历史位置失效的情况,产品可以改为先提示人工选择。无论选哪种策略,计算结果不应直接覆盖批注的anchor字段。只有布局版本确认保存后,labelBox才能进入持久层,屏幕缩放和滚动仍由视图矩阵计算。

批注矩形避让算法与开发环境布局

五、围绕稳定性设计排查步骤

当用户反馈“缩放后文字乱跑”,第一步应核对保存的原始页锚点是否变化。若anchor没有变化而labelBox变化,继续检查排布配置版本、字体测量缓存键和可见图层集合。若两者同时变化,需要回查触点逆矩阵和批注编辑提交路径,避免把视图拖动误识别为修改业务位置。

当用户反馈“隐藏一个图层后其他标签仍占着空位”,检查占用矩形表是否仅由当前可见、有效的图层条目构成;当两个标签重叠,查看优先级排序与候选枚举顺序是否固定;当有标签消失,必须查询unplaced列表并明确展示数量。建议调试信息记录labelId、anchor、measuredSize、candidateIndex、layoutVersion和rejectReason,而不是反复打印整张图纸的内容。

重排过程可能触发多次,比如字体加载、窗口宽度更新和图层开关同时发生。可以用递增的layoutGeneration标记计算结果,仅允许当前代际的结果进入界面。页面离开后取消待提交的布局刷新,清空测量缓存中已失效的条目;绘制阶段使用的临时画布对象按组件生命周期清理,避免大量图纸页切换后积累无用资源。

图纸文本标签避让后的界面结构

六、文字位置可以变,工程定位不能漂移

文本框重排解决的是可读性问题,不能反向改写工程实体的坐标。若同一图纸在另一台大屏以不同尺寸打开,系统应先恢复固定页锚点,再按确定的排布策略选择标签框位置;用户手工固定的布局需要明确适用的图纸版本与字体配置。对于已经被其他人员调整过的标签,保存时应比对revision,冲突时提供差异而不是静默覆盖。

这一章延续了前四篇的页坐标、逆矩阵、图层独立保存和输入仲裁。新增的只是文字测量与稳定布局层:明确候选、优先级和失败出口,既能提升密集机电图纸的可读性,也保留精确的设备锚点。下一篇会把同一张大图纸拆为可见区分块加载,届时文字布局仍应只依据原始页空间和有效图层,而不受暂时载入了哪些图块影响。

华为开发者文档可参考ArkGraphics 2D文本开发总览、Text模块文字绘制与布局和Canvas画布变换与状态。测量由图形接口提供数据,候选位置、冲突优先级与稳定保存规则由应用侧实现。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐