195 lines
7.6 KiB
Matlab
195 lines
7.6 KiB
Matlab
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
|
|
|