This commit is contained in:
Horldsence
2026-09-13 10:27:09 +08:00
parent b810ec7777
commit 82311a47cc
52 changed files with 2563 additions and 1278 deletions
+194
View File
@@ -0,0 +1,194 @@
function H = geom_helpers()
%GEOM_HELPERS 几何示意图共用的绘制函数。
% 只使用 MATLAB 默认图形对象与默认配色,不做自定义样式。
H.circle = @draw_circle;
H.wedge = @draw_wedge;
H.disk = @draw_disk;
H.poly = @draw_poly;
H.marker = @draw_marker;
H.arrow = @draw_arrow;
H.label_points = @label_points;
H.text_width = @text_width;
end
% ---------------------------------------------------------------------------
% 基本图元
% ---------------------------------------------------------------------------
function h = draw_circle(ax, center, radius, color, lw, style)
t = linspace(0, 2 * pi, 360);
h = plot(ax, center(1) + radius * cos(t), center(2) + radius * sin(t), ...
'Color', color, 'LineWidth', lw, 'LineStyle', style);
end
function h = draw_wedge(ax, poly, color, alpha)
h = patch(ax, 'XData', poly(:, 1), 'YData', poly(:, 2), 'FaceColor', color, ...
'FaceAlpha', alpha, 'EdgeColor', 'none');
end
function h = draw_disk(ax, center, radius, color, alpha, edge_style)
t = linspace(0, 2 * pi, 360);
h = patch(ax, 'XData', center(1) + radius * cos(t), ...
'YData', center(2) + radius * sin(t), ...
'FaceColor', color, 'FaceAlpha', alpha, 'EdgeColor', color, ...
'LineWidth', 0.6, 'LineStyle', edge_style);
end
function h = draw_poly(ax, poly, face, alpha, edge, lw)
h = patch(ax, 'XData', poly(:, 1), 'YData', poly(:, 2), 'FaceColor', face, ...
'FaceAlpha', alpha, 'EdgeColor', edge, 'LineWidth', lw);
end
function h = draw_marker(ax, p, color, size, filled)
if filled
h = plot(ax, p(1), p(2), 'o', 'MarkerSize', size, ...
'MarkerFaceColor', color, 'MarkerEdgeColor', 'none');
else
h = plot(ax, p(1), p(2), 'o', 'MarkerSize', size, ...
'MarkerFaceColor', 'none', 'MarkerEdgeColor', color, ...
'LineWidth', 0.9);
end
end
function h = draw_arrow(ax, p0, p1, color)
%DRAW_ARROW 数据坐标箭头:直线 + 实心三角箭头。
% 箭头在"像素空间"里构造后换算回数据坐标,因此与坐标轴比例无关,
% 也不依赖绘制时的 axes 位置(图例把坐标区挤小后箭头依然对准数据点)。
h = plot(ax, [p0(1) p1(1)], [p0(2) p1(2)], 'Color', color, 'LineWidth', 0.9);
d = [p1(1) - p0(1), p1(2) - p0(2)];
[sx, sy] = px_scale(ax); % 每像素对应的数据单位
v = [d(1) / sx, d(2) / sy]; % 像素空间向量
len = hypot(v(1), v(2));
if len < 1e-9
return;
end
u = v / len;
n = [-u(2), u(1)];
head = min(len * 0.6, 10); % 头长(像素)
base = v - u * head;
tri = [v; base + n * head * 0.42; base - n * head * 0.42] .* [sx, sy];
patch(ax, 'XData', p0(1) + tri(:, 1), 'YData', p0(2) + tri(:, 2), ...
'FaceColor', color, 'EdgeColor', 'none');
end
function [sx, sy] = px_scale(ax)
pos = ax.Position;
fpos = ancestor(ax, 'figure').Position;
sx = diff(ax.XLim) / (pos(3) * fpos(3));
sy = diff(ax.YLim) / (pos(4) * fpos(4));
end
% ---------------------------------------------------------------------------
% 标签自动避让
% ---------------------------------------------------------------------------
function label_points(ax, pts, labels, color)
%LABEL_POINTS 在散点旁放置标签:按候选方位打分选出与已放标签、标记点都不重叠的位置。
% color 可以是 1×3(所有标签同色)或 n×3(逐点配色)。
if isempty(pts)
return;
end
if size(color, 1) == 1
color = repmat(color, numel(labels), 1);
end
xl = ax.XLim;
yl = ax.YLim;
gap = pixel_gap(ax, 5); % 标签与标记点之间的固定像素间距
mbox = marker_size(ax) * [-0.5 -0.5 1 1];
placed = zeros(0, 4);
for i = 1:numel(labels)
if pts(i, 1) < xl(1) || pts(i, 1) > xl(2) || ...
pts(i, 2) < yl(1) || pts(i, 2) > yl(2)
continue; % 视野外的点不标注
end
sz = text_size(ax, labels{i});
cand = candidates(pts(i, :), sz, gap);
best_pen = inf;
best = cand(1, :);
for k = 1:size(cand, 1)
box = [cand(k, 1) - sz(1) / 2 - gap(1), cand(k, 2) - sz(2) / 2 - gap(2), ...
sz(1) + 2 * gap(1), sz(2) + 2 * gap(2)];
pen = penalty(box, placed, pts, mbox, xl, yl);
if pen < best_pen
best_pen = pen;
best = cand(k, :);
end
end
text(ax, best(1), best(2), labels{i}, 'Color', color(i, :), ...
'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle');
placed(end + 1, :) = [best(1) - sz(1) / 2 - gap(1), ...
best(2) - sz(2) / 2 - gap(2), ...
sz(1) + 2 * gap(1), sz(2) + 2 * gap(2)]; %#ok<AGROW>
end
end
function cand = candidates(p, sz, gap)
%CANDIDATES 标记点周围的候选标签中心:先近后远,四正方向 + 四斜角。
dx = gap(1) + sz(1) / 2;
dy = gap(2) + sz(2) / 2;
cand = [p(1) + dx, p(2); p(1) - dx, p(2)
p(1), p(2) + dy; p(1), p(2) - dy
p(1) + dx, p(2) + dy; p(1) - dx, p(2) + dy
p(1) + dx, p(2) - dy; p(1) - dx, p(2) - dy
p(1) + 2.2 * dx, p(2); p(1) - 2.2 * dx, p(2)
p(1), p(2) + 2.2 * dy; p(1), p(2) - 2.2 * dy
p(1) + 1.9 * dx, p(2) + 1.9 * dy; p(1) - 1.9 * dx, p(2) + 1.9 * dy
p(1) + 1.9 * dx, p(2) - 1.9 * dy; p(1) - 1.9 * dx, p(2) - 1.9 * dy
p(1) + 3.4 * dx, p(2); p(1) - 3.4 * dx, p(2)
p(1), p(2) + 3.4 * dy; p(1), p(2) - 3.4 * dy];
end
function g = pixel_gap(ax, npix)
pos = ax.Position;
fpos = ancestor(ax, 'figure').Position;
g = [npix * diff(ax.XLim) / (pos(3) * fpos(3)), ...
npix * diff(ax.YLim) / (pos(4) * fpos(4))];
end
function pen = penalty(box, placed, pts, mbox, xl, yl)
pen = 0;
for j = 1:size(placed, 1) % 标签之间重叠代价最高
pen = pen + 12 * overlap(box, placed(j, :));
end
for j = 1:size(pts, 1) % 其次避免压住标记点
mb = [pts(j, 1) + mbox(1), pts(j, 2) + mbox(2), mbox(3), mbox(4)];
pen = pen + 3 * overlap(box, mb);
end
dx = max(0, xl(1) - box(1)) + max(0, box(1) + box(3) - xl(2));
dy = max(0, yl(1) - box(2)) + max(0, box(2) + box(4) - yl(2));
pen = pen + 1e3 * (dx * box(4) + dy * box(3));
end
function a = overlap(b1, b2)
w = min(b1(1) + b1(3), b2(1) + b2(3)) - max(b1(1), b2(1));
h = min(b1(2) + b1(4), b2(2) + b2(4)) - max(b1(2), b2(2));
a = max(0, w) * max(0, h);
end
function sz = text_size(ax, str)
%TEXT_SIZE 文本在数据坐标下的包围盒尺寸(用临时 text 对象的 Extent 量取)。
h = text(ax, 0, 0, str, 'Units', 'data', 'Visible', 'off', ...
'Interpreter', 'none');
e = get(h, 'Extent');
delete(h);
sz = [e(3) e(4)] * 1.12;
end
function w = text_width(ax, str, fs)
%TEXT_WIDTH 指定字号下文本在数据坐标下的宽度。
h = text(ax, 0, 0, str, 'Units', 'data', 'Visible', 'off', ...
'FontSize', fs, 'Interpreter', 'none');
e = get(h, 'Extent');
delete(h);
w = e(3) * 1.04;
end
function s = marker_size(ax)
xl = ax.XLim;
yl = ax.YLim;
pos = ax.Position;
fpos = ancestor(ax, 'figure').Position;
w_px = pos(3) * fpos(3);
h_px = pos(4) * fpos(4);
s = 8 * max(diff(xl) / w_px, diff(yl) / h_px); % 约 8 px
end