123
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
function fig_flow_overall()
|
||||
%FIG_FLOW_OVERALL 四层主线:题面抽象(L0)、在线三层(L1)、四问主线(L2)、三条闭环(L3)。
|
||||
% 仅保留分层框图与层标签,图内不放任何说明性文字。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'flow_overall');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
ax = axes(fig, 'Position', [0.02 0.06 0.96 0.90], ...
|
||||
'XLim', [0 100], 'YLim', [0 100], 'Visible', 'off');
|
||||
hold(ax, 'on');
|
||||
|
||||
n = numel(D.tags);
|
||||
x_tag = 7; % 层标签中心
|
||||
x_lo = 15; x_hi = 99; % 框体水平范围
|
||||
box_gap = 2.2; % 框间距(放箭头)
|
||||
pad = 1.4; % 框内左右留白
|
||||
band_h = 14; % 层高
|
||||
band_gap = 6.5;
|
||||
y_top = 96;
|
||||
|
||||
% 统一字号:逐行按量得的文本宽度试算,保证一排框加间距放得下
|
||||
fs = ax.FontSize;
|
||||
while fs > 8
|
||||
need_max = 0;
|
||||
for b = 1:n
|
||||
labels = D.labels{b};
|
||||
m = numel(labels);
|
||||
need = sum(cellfun(@(s) H.text_width(ax, s, fs), labels)) ...
|
||||
+ 2 * pad * m + box_gap * (m - 1);
|
||||
need_max = max(need_max, need);
|
||||
end
|
||||
if need_max <= x_hi - x_lo
|
||||
break;
|
||||
end
|
||||
fs = fs - 0.5;
|
||||
end
|
||||
|
||||
for b = 1:n
|
||||
labels = D.labels{b};
|
||||
m = numel(labels);
|
||||
tw = cellfun(@(s) H.text_width(ax, s, fs), labels);
|
||||
avail = (x_hi - x_lo) - box_gap * (m - 1);
|
||||
w = (tw + 2 * pad) * avail / sum(tw + 2 * pad);
|
||||
y = y_top - band_h - (b - 1) * (band_h + band_gap);
|
||||
color = pick_color(S, D.colors{b});
|
||||
text(ax, x_tag, y + band_h / 2, D.tags{b}, 'FontSize', fs + 1, ...
|
||||
'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle');
|
||||
x = x_lo;
|
||||
for i = 1:m
|
||||
rectangle(ax, 'Position', [x, y, w(i), band_h], 'FaceColor', 'w', ...
|
||||
'EdgeColor', color, 'LineWidth', 1.1);
|
||||
text(ax, x + w(i) / 2, y + band_h / 2, labels{i}, 'FontSize', fs, ...
|
||||
'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle', ...
|
||||
'Interpreter', 'none');
|
||||
if i < m
|
||||
H.arrow(ax, [x + w(i) + 0.25, y + band_h / 2], ...
|
||||
[x + w(i) + box_gap - 0.25, y + band_h / 2], color);
|
||||
end
|
||||
x = x + w(i) + box_gap;
|
||||
end
|
||||
end
|
||||
hold(ax, 'off');
|
||||
fig_save(fig, S, 'flow-overall-architecture.png');
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
function fig_geo_q1_counterexample()
|
||||
%FIG_GEO_Q1_COUNTEREXAMPLE 问题一:等边三角形布站时直径圆不覆盖定位区域的反例。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'geo_q1_counterexample');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
t = tiledlayout(fig, 1, 2, 'TileSpacing', 'compact', 'Padding', 'compact');
|
||||
|
||||
% 左:角域交会总览
|
||||
ax1 = nexttile(t);
|
||||
hold(ax1, 'on');
|
||||
hw = gobjects(0);
|
||||
for i = 1:size(D.wedges, 1)
|
||||
hw = H.wedge(ax1, squeeze(D.wedges(i, :, :)), pick_color(S, 'primary'), 0.25);
|
||||
end
|
||||
hr = plot(ax1, D.zoom_center(1), D.zoom_center(2), 's', 'MarkerSize', 6, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
plot(ax1, D.stations(:, 1), D.stations(:, 2), 'o', 'MarkerSize', 6, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
H.label_points(ax1, D.stations, D.station_labels, pick_color(S, 'accent'));
|
||||
hold(ax1, 'off');
|
||||
axis(ax1, 'equal');
|
||||
xlim(ax1, D.view_center(1) + [-D.half_overview, D.half_overview]);
|
||||
ylim(ax1, D.view_center(2) + [-D.half_overview, D.half_overview]);
|
||||
xlabel(ax1, 'x / m');
|
||||
ylabel(ax1, 'y / m');
|
||||
legend(ax1, [hw(1), hr], D.legend1, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
% 右:定位区域放大
|
||||
ax2 = nexttile(t);
|
||||
hold(ax2, 'on');
|
||||
for i = 1:size(D.zoom_wedges, 1)
|
||||
H.wedge(ax2, squeeze(D.zoom_wedges(i, :, :)), pick_color(S, 'primary'), 0.25);
|
||||
end
|
||||
H.poly(ax2, D.zoom_region, pick_color(S, 'accent'), 1, 'none', 0.5);
|
||||
hd = H.circle(ax2, D.dc_center, D.dc_radius, pick_color(S, 'violet'), 1.2, '--');
|
||||
hm = H.circle(ax2, D.mec_center, D.mec_radius, pick_color(S, 'success'), 1.4, '-');
|
||||
H.marker(ax2, D.dc_center, pick_color(S, 'violet'), 5, true);
|
||||
H.marker(ax2, D.mec_center, pick_color(S, 'success'), 5, true);
|
||||
hold(ax2, 'off');
|
||||
axis(ax2, 'equal');
|
||||
xlim(ax2, D.zoom_center(1) + [-D.zoom_half, D.zoom_half]);
|
||||
ylim(ax2, D.zoom_center(2) + [-D.zoom_half, D.zoom_half]);
|
||||
xlabel(ax2, 'x / m');
|
||||
ylabel(ax2, 'y / m');
|
||||
legend(ax2, [hd, hm], D.legend2, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
fig_save(fig, S, 'geo-q1-diameter-circle-counterexample.png');
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
function fig_geo_q1_meetup()
|
||||
%FIG_GEO_Q1_MEETUP 问题一:角域交会总览与定位区域放大(直径圆、最小包围圆)。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'geo_q1_meetup');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
t = tiledlayout(fig, 1, 2, 'TileSpacing', 'compact', 'Padding', 'compact');
|
||||
|
||||
% 左:角域交会总览(交会区域在总览里只是一个点,放大后见右图)
|
||||
ax1 = nexttile(t);
|
||||
hold(ax1, 'on');
|
||||
hw = gobjects(0);
|
||||
for i = 1:size(D.wedges, 1)
|
||||
hw = H.wedge(ax1, squeeze(D.wedges(i, :, :)), pick_color(S, 'primary'), 0.25);
|
||||
end
|
||||
hr = plot(ax1, D.zoom_center(1), D.zoom_center(2), 's', 'MarkerSize', 6, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
plot(ax1, D.stations(:, 1), D.stations(:, 2), 'o', 'MarkerSize', 6, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
H.label_points(ax1, D.stations, D.station_labels, pick_color(S, 'accent'));
|
||||
hold(ax1, 'off');
|
||||
axis(ax1, 'equal');
|
||||
xlim(ax1, D.view_center(1) + [-D.half_overview, D.half_overview]);
|
||||
ylim(ax1, D.view_center(2) + [-D.half_overview, D.half_overview]);
|
||||
xlabel(ax1, 'x / m');
|
||||
ylabel(ax1, 'y / m');
|
||||
legend(ax1, [hw(1), hr], D.legend1, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
% 右:定位区域放大
|
||||
ax2 = nexttile(t);
|
||||
hold(ax2, 'on');
|
||||
for i = 1:size(D.zoom_wedges, 1)
|
||||
H.wedge(ax2, squeeze(D.zoom_wedges(i, :, :)), pick_color(S, 'primary'), 0.25);
|
||||
end
|
||||
H.poly(ax2, D.zoom_region, pick_color(S, 'accent'), 1, 'none', 0.5);
|
||||
hd = H.circle(ax2, D.dc_center, D.dc_radius, pick_color(S, 'violet'), 1.2, '--');
|
||||
hm = H.circle(ax2, D.mec_center, D.mec_radius, pick_color(S, 'success'), 1.4, '-');
|
||||
H.marker(ax2, D.dc_center, pick_color(S, 'violet'), 5, true);
|
||||
H.marker(ax2, D.mec_center, pick_color(S, 'success'), 5, true);
|
||||
hold(ax2, 'off');
|
||||
axis(ax2, 'equal');
|
||||
xlim(ax2, D.zoom_center(1) + [-D.zoom_half, D.zoom_half]);
|
||||
ylim(ax2, D.zoom_center(2) + [-D.zoom_half, D.zoom_half]);
|
||||
xlabel(ax2, 'x / m');
|
||||
ylabel(ax2, 'y / m');
|
||||
legend(ax2, [hd, hm], D.legend2, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
fig_save(fig, S, 'geo-q1-convex-intersection-zoom.png');
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
function fig_geo_q2_safe_disk()
|
||||
%FIG_GEO_Q2_SAFE_DISK 问题二:截断角域外接圆与保证接收的安全候选盘。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'geo_q2');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
t = tiledlayout(fig, 1, 2, 'TileSpacing', 'compact', 'Padding', 'compact');
|
||||
|
||||
% 左:真实几何(Ω 内的角域与两个候选圆)
|
||||
ax1 = nexttile(t);
|
||||
hold(ax1, 'on');
|
||||
H.circle(ax1, [0 0], D.omega_r, pick_color(S, 'primary'), 1.2, '-');
|
||||
hw = H.wedge(ax1, D.wedge, pick_color(S, 'accent'), 0.25);
|
||||
hd = H.circle(ax1, D.c, D.a, pick_color(S, 'violet'), 1.2, '--');
|
||||
hs = H.disk(ax1, D.c, D.safe, pick_color(S, 'success'), 0.30, '-');
|
||||
H.marker(ax1, [0 0], pick_color(S, 'accent'), 6, true);
|
||||
H.marker(ax1, D.c, pick_color(S, 'violet'), 5, true);
|
||||
H.label_points(ax1, [0 0], {'s1'}, pick_color(S, 'accent'));
|
||||
H.label_points(ax1, D.c, {'c'}, pick_color(S, 'violet'));
|
||||
hold(ax1, 'off');
|
||||
axis(ax1, 'equal');
|
||||
xlim(ax1, [-D.half, D.half]);
|
||||
ylim(ax1, [-D.half, D.half]);
|
||||
xlabel(ax1, 'x / m');
|
||||
ylabel(ax1, 'y / m');
|
||||
legend(ax1, [hw, hd, hs], D.legend, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
% 右:半径分解 a + (1000 − a) = 1000 m
|
||||
ax2 = nexttile(t);
|
||||
hold(ax2, 'on');
|
||||
H.circle(ax2, [0 0], D.a, pick_color(S, 'violet'), 1.2, '--');
|
||||
H.disk(ax2, [0 0], D.safe, pick_color(S, 'success'), 0.30, '-');
|
||||
H.marker(ax2, [0 0], 'k', 5, true);
|
||||
ang = deg2rad(35);
|
||||
p_out = D.a * [cos(ang), sin(ang)];
|
||||
p_in = D.safe * [cos(-ang), sin(-ang)];
|
||||
H.arrow(ax2, [0 0], p_out, pick_color(S, 'violet'));
|
||||
H.arrow(ax2, [0 0], p_in, pick_color(S, 'success'));
|
||||
text(ax2, p_out(1) * 1.06, p_out(2) * 1.12, sprintf('a = %.1f m', D.a), ...
|
||||
'Color', pick_color(S, 'violet'), 'Interpreter', 'none');
|
||||
text(ax2, p_in(1) * 1.04, p_in(2) * 1.30, sprintf('1000 − a = %.1f m', D.safe), ...
|
||||
'Color', pick_color(S, 'success'), 'HorizontalAlignment', 'right', ...
|
||||
'Interpreter', 'none');
|
||||
text(ax2, -D.safe * 0.45, 0, 'c', 'Color', 'k', ...
|
||||
'HorizontalAlignment', 'right', 'VerticalAlignment', 'middle');
|
||||
hold(ax2, 'off');
|
||||
axis(ax2, 'equal');
|
||||
xlim(ax2, [-D.a * 1.15, D.a * 1.75]);
|
||||
ylim(ax2, [-D.a * 1.05, D.a * 1.05]);
|
||||
axis(ax2, 'off');
|
||||
|
||||
fig_save(fig, S, 'geo-q2-guaranteed-reception-disk.png');
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
function fig_geo_q3_coverage()
|
||||
%FIG_GEO_Q3_COVERAGE 问题三:七个 1000 m 接收盘覆盖区域 Ω。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'geo_q3');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
ax = axes(fig);
|
||||
hold(ax, 'on');
|
||||
|
||||
hd = gobjects(0);
|
||||
for i = 1:size(D.stations, 1)
|
||||
hd = H.disk(ax, D.stations(i, :), D.r_eff, pick_color(S, 'success'), 0.18, '-');
|
||||
end
|
||||
hw = H.circle(ax, [0 0], D.omega_r, pick_color(S, 'primary'), 1.2, '-');
|
||||
hs = plot(ax, D.stations(:, 1), D.stations(:, 2), 'o', 'MarkerSize', 6, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
hx = H.marker(ax, D.worst, 'k', 8, false);
|
||||
hold(ax, 'off');
|
||||
axis(ax, 'equal');
|
||||
xlim(ax, [-D.half, D.half]);
|
||||
ylim(ax, [-D.half, D.half]);
|
||||
xlabel(ax, 'x / m');
|
||||
ylabel(ax, 'y / m');
|
||||
legend(ax, [hw, hd(1), hs, hx], D.legend, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
fig_save(fig, S, 'geo-q3-seven-point-coverage.png');
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
function fig_geo_q4_mincover()
|
||||
%FIG_GEO_Q4_MINCOVER 问题四:31 点最小命中集与前向半盘覆盖语义。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'geo_q4');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
t = tiledlayout(fig, 1, 2, 'TileSpacing', 'compact', 'Padding', 'compact');
|
||||
|
||||
% 左:Ω 内的 31 个覆盖点
|
||||
ax1 = nexttile(t);
|
||||
hold(ax1, 'on');
|
||||
hw = H.circle(ax1, [0 0], D.omega_r, pick_color(S, 'primary'), 1.2, '-');
|
||||
hp = plot(ax1, D.points(:, 1), D.points(:, 2), 'o', 'MarkerSize', 5, ...
|
||||
'MarkerFaceColor', pick_color(S, 'accent'), 'MarkerEdgeColor', 'none');
|
||||
hold(ax1, 'off');
|
||||
axis(ax1, 'equal');
|
||||
xlim(ax1, [-D.half, D.half]);
|
||||
ylim(ax1, [-D.half, D.half]);
|
||||
xlabel(ax1, 'x / m');
|
||||
ylabel(ax1, 'y / m');
|
||||
legend(ax1, [hw, hp], D.legend, 'Location', 'southoutside', ...
|
||||
'Orientation', 'horizontal');
|
||||
|
||||
% 右:前向半盘(朝向未知,±90°)
|
||||
ax2 = nexttile(t);
|
||||
hold(ax2, 'on');
|
||||
H.poly(ax2, D.half_disk, pick_color(S, 'accent'), 0.30, 'none', 0.5);
|
||||
H.circle(ax2, [0 0.5], 0.85, pick_color(S, 'muted'), 0.9, '-');
|
||||
plot(ax2, [0 0], [0.5 - 0.85, 0.5 + 0.85], '--', ...
|
||||
'Color', pick_color(S, 'accent'), 'LineWidth', 1.0);
|
||||
H.arrow(ax2, [0 0.5], [0.85 0.5], pick_color(S, 'accent'));
|
||||
text(ax2, 0.425, 0.30, D.radius_label, 'HorizontalAlignment', 'center', ...
|
||||
'Interpreter', 'none');
|
||||
text(ax2, 0.0, 1.55, D.heading_label, 'HorizontalAlignment', 'center', ...
|
||||
'Interpreter', 'none');
|
||||
hold(ax2, 'off');
|
||||
axis(ax2, 'equal');
|
||||
xlim(ax2, [-1.15 1.35]);
|
||||
ylim(ax2, [-0.55 1.75]);
|
||||
axis(ax2, 'off');
|
||||
|
||||
fig_save(fig, S, 'geo-q4-minimum-cover-31-points.png');
|
||||
end
|
||||
@@ -0,0 +1,109 @@
|
||||
function fig_replay(name)
|
||||
%FIG_REPLAY 回放画布(点运动图):世界视图 + 事件流水 + 虚拟时间轴。
|
||||
% name: 'replay_q3' 或 'replay_q4',对应 docs/figures/.data/<name>.mat。
|
||||
% 本图是唯一保留自定义画布版式的图:三个区互不重叠,图例置于世界视图下方。
|
||||
|
||||
if nargin < 1
|
||||
name = 'replay_q3';
|
||||
end
|
||||
S = fig_style();
|
||||
D = load_data(S, name);
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off', 'Position', [100 100 1040 660]);
|
||||
|
||||
c_path = [0.35 0.35 0.35];
|
||||
c_ray = pick_color(S, 'primary');
|
||||
c_solved = pick_color(S, 'success');
|
||||
c_unsolv = pick_color(S, 'accent');
|
||||
c_head = pick_color(S, 'violet');
|
||||
|
||||
% ---------------- 世界视图 ----------------
|
||||
ax = axes(fig, 'Position', [0.075 0.40 0.49 0.56]);
|
||||
hold(ax, 'on');
|
||||
H.circle(ax, [0 0], D.omega_r, pick_color(S, 'primary'), 1.2, '-');
|
||||
h_path = plot(ax, D.path(:, 1), D.path(:, 2), 'Color', c_path, 'LineWidth', 0.9);
|
||||
h_ray = plot(ax, nan, nan, 'Color', c_ray, 'LineWidth', 0.8);
|
||||
for i = 1:size(D.rays, 1)
|
||||
p0 = D.rays(i, 1:2);
|
||||
p1 = p0 + D.rays(i, 3:4);
|
||||
plot(ax, [p0(1) p1(1)], [p0(2) p1(2)], 'Color', c_ray, 'LineWidth', 0.8);
|
||||
end
|
||||
h_disk = gobjects(0);
|
||||
for i = 1:size(D.half_disks, 1)
|
||||
disk = squeeze(D.half_disks(i, :, :));
|
||||
h_disk(end + 1) = patch(ax, 'XData', disk(:, 1), 'YData', disk(:, 2), ...
|
||||
'FaceColor', c_head, 'FaceAlpha', 0.10, ...
|
||||
'EdgeColor', c_head, 'LineWidth', 0.8); %#ok<AGROW>
|
||||
end
|
||||
for i = 1:size(D.headings, 1)
|
||||
p0 = D.headings(i, 1:2);
|
||||
p1 = p0 + D.headings(i, 3:4);
|
||||
H.arrow(ax, p0, p1, c_head);
|
||||
end
|
||||
solved = D.truth_solved > 0.5;
|
||||
h_solved = plot(ax, D.truth(solved, 1), D.truth(solved, 2), 'o', ...
|
||||
'MarkerSize', 6, 'MarkerFaceColor', c_solved, 'MarkerEdgeColor', 'none');
|
||||
h_unsolv = plot(ax, D.truth(~solved, 1), D.truth(~solved, 2), 'o', ...
|
||||
'MarkerSize', 6, 'MarkerFaceColor', c_unsolv, 'MarkerEdgeColor', 'none');
|
||||
label_color = repmat(c_unsolv, size(D.truth, 1), 1);
|
||||
label_color(solved, :) = repmat(c_solved, nnz(solved), 1);
|
||||
H.label_points(ax, D.truth, D.truth_labels, label_color);
|
||||
plot(ax, D.clears(D.clears(:, 3) > 0.5, 1), D.clears(D.clears(:, 3) > 0.5, 2), ...
|
||||
'x', 'MarkerSize', 7, 'LineWidth', 1.4, 'Color', c_solved);
|
||||
plot(ax, D.clears(D.clears(:, 3) <= 0.5, 1), D.clears(D.clears(:, 3) <= 0.5, 2), ...
|
||||
'o', 'MarkerSize', 6, 'MarkerEdgeColor', 'k', 'MarkerFaceColor', 'none');
|
||||
hold(ax, 'off');
|
||||
axis(ax, 'equal');
|
||||
xlim(ax, [-D.half, D.half]);
|
||||
ylim(ax, [-D.half, D.half]);
|
||||
xlabel(ax, 'x / m');
|
||||
ylabel(ax, 'y / m');
|
||||
hleg = [h_path, h_ray];
|
||||
if ~isempty(h_disk)
|
||||
hleg(end + 1) = h_disk(1);
|
||||
end
|
||||
hleg = [hleg, h_solved, h_unsolv];
|
||||
legend(ax, hleg, D.legend, 'Location', 'southoutside', 'Orientation', 'horizontal');
|
||||
|
||||
% ---------------- 事件流水 ----------------
|
||||
ex = axes(fig, 'Position', [0.625 0.40 0.345 0.56], ...
|
||||
'XLim', [0 1], 'YLim', [0 1], 'Visible', 'off');
|
||||
hold(ex, 'on');
|
||||
cols = [0.00 0.34 1.00];
|
||||
align = {'left', 'left', 'right'};
|
||||
head = {'动作', '参数', 't / s'};
|
||||
for j = 1:3
|
||||
text(ex, cols(j), 0.97, head{j}, 'FontWeight', 'bold', ...
|
||||
'HorizontalAlignment', align{j});
|
||||
end
|
||||
for i = 1:size(D.events, 1)
|
||||
y = 0.90 - (i - 1) * 0.088;
|
||||
for j = 1:3
|
||||
text(ex, cols(j), y, D.events{i, j}, 'HorizontalAlignment', align{j}, ...
|
||||
'Interpreter', 'none');
|
||||
end
|
||||
end
|
||||
hold(ex, 'off');
|
||||
|
||||
% ---------------- 虚拟时间轴 ----------------
|
||||
tx = axes(fig, 'Position', [0.075 0.155 0.90 0.125]);
|
||||
hold(tx, 'on');
|
||||
rectangle(tx, 'Position', [0 0.34 D.total_s 0.32], ...
|
||||
'FaceColor', pick_color(S, 'primary'), 'FaceAlpha', 0.12, ...
|
||||
'EdgeColor', 'none');
|
||||
for i = 1:size(D.marks, 1)
|
||||
t = D.marks(i, 1);
|
||||
if D.marks(i, 3) > 0.5
|
||||
plot(tx, [t t], [0.05 0.95], 'Color', c_solved, 'LineWidth', 1.0);
|
||||
elseif D.marks(i, 2) > 0.5
|
||||
plot(tx, [t t], [0.22 0.78], 'Color', c_unsolv, 'LineWidth', 0.8);
|
||||
end
|
||||
end
|
||||
hold(tx, 'off');
|
||||
xlim(tx, [0 D.total_s]);
|
||||
ylim(tx, [0 1]);
|
||||
set(tx, 'YTick', []);
|
||||
xlabel(tx, '虚拟时间 t / s');
|
||||
|
||||
fig_save(fig, S, D.out_png{1});
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
function fig_save(fig, S, name)
|
||||
%FIG_SAVE 导出白底 PNG(300 dpi)并关闭图形窗口。
|
||||
% 导出前关掉坐标轴工具条,避免交互按钮被画进图片。
|
||||
axs = findall(fig, 'Type', 'axes');
|
||||
for k = 1:numel(axs)
|
||||
axs(k).Toolbar.Visible = 'off';
|
||||
end
|
||||
exportgraphics(fig, fullfile(S.out_dir, name), 'Resolution', S.dpi, ...
|
||||
'BackgroundColor', 'white');
|
||||
close(fig);
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
function fig_steps_state_machine()
|
||||
%FIG_STEPS_STATE_MACHINE 会话状态机:IDLE → 倒计时 → OPEN → CLOSED。
|
||||
% 只画状态框与迁移箭头,图内不放任何说明性文字。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'steps_state_machine');
|
||||
H = geom_helpers();
|
||||
fig = figure('Color', 'w', 'Visible', 'off', 'Position', [100 100 900 240]);
|
||||
ax = axes(fig, 'Position', [0.02 0.10 0.96 0.80], ...
|
||||
'XLim', [0 100], 'YLim', [0 100], 'Visible', 'off');
|
||||
hold(ax, 'on');
|
||||
|
||||
n = numel(D.labels);
|
||||
w = 21;
|
||||
gap = 4.0;
|
||||
x0 = (100 - (n * w + (n - 1) * gap)) / 2;
|
||||
y0 = 25;
|
||||
h = 50;
|
||||
for i = 1:n
|
||||
x = x0 + (i - 1) * (w + gap);
|
||||
color = pick_color(S, D.edges{i});
|
||||
rectangle(ax, 'Position', [x, y0, w, h], 'FaceColor', 'w', ...
|
||||
'EdgeColor', color, 'LineWidth', 1.2);
|
||||
text(ax, x + w / 2, y0 + h / 2, D.labels{i}, ...
|
||||
'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle', ...
|
||||
'Interpreter', 'none');
|
||||
if i < n
|
||||
H.arrow(ax, [x + w + 0.5, y0 + h / 2], ...
|
||||
[x + w + gap - 0.5, y0 + h / 2], color);
|
||||
end
|
||||
end
|
||||
hold(ax, 'off');
|
||||
fig_save(fig, S, 'steps-state-machine.png');
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
function S = fig_style()
|
||||
%FIG_STYLE 出图环境:目录、导出参数与 MATLAB 默认配色。
|
||||
% 除 fig_replay(回放画布,点运动图)外,各图一律使用 MATLAB 默认图形样式:
|
||||
% 默认配色、默认字体、默认坐标轴与图例;本函数不设置任何自定义样式。
|
||||
|
||||
root = fileparts(fileparts(fileparts(mfilename('fullpath'))));
|
||||
S.data_dir = fullfile(root, 'docs', 'figures', '.data');
|
||||
S.out_dir = fullfile(root, 'docs', 'figures');
|
||||
S.dpi = 300; % 论文插图导出分辨率
|
||||
S.c = get(groot, 'defaultAxesColorOrder'); % 默认配色(7×3)
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
function fig_timing_attachment()
|
||||
%FIG_TIMING_ATTACHMENT 附件计时示例:逐动作的耗时构成(堆叠条形)与单步合计。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'timing_attachment');
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
ax = axes(fig);
|
||||
|
||||
n = numel(D.labels);
|
||||
Y = [D.move(:), D.switch_s(:), D.action(:)];
|
||||
y = (n:-1:1)'; % 动作按先后从上到下排列
|
||||
barh(ax, y, Y, 'stacked');
|
||||
total = sum(Y, 2);
|
||||
xlim(ax, [0, max(total) * 1.18]);
|
||||
hold(ax, 'on');
|
||||
for i = 1:n
|
||||
if total(i) > 0
|
||||
text(ax, total(i) + max(total) * 0.02, y(i), ...
|
||||
sprintf('%.0f s', total(i)), 'VerticalAlignment', 'middle', ...
|
||||
'Interpreter', 'none');
|
||||
end
|
||||
end
|
||||
hold(ax, 'off');
|
||||
set(ax, 'YTick', 1:n, 'YTickLabel', flipud(D.labels(:)), ...
|
||||
'TickLabelInterpreter', 'none');
|
||||
xlabel(ax, D.xlabel);
|
||||
legend(ax, D.legend);
|
||||
fig_save(fig, S, 'timing-attachment-example-105-111-194-199.png');
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
function fig_timing_measured()
|
||||
%FIG_TIMING_MEASURED 实测一局:累计虚拟时间按动作类型的堆叠构成。
|
||||
|
||||
S = fig_style();
|
||||
D = load_data(S, 'timing_measured');
|
||||
fig = figure('Color', 'w', 'Visible', 'off');
|
||||
ax = axes(fig);
|
||||
|
||||
seq = D.seq(:);
|
||||
bands = [D.cum(:, 1), diff(D.cum, 1, 2)]; % area 对矩阵按列堆叠,这里给每层厚度
|
||||
h = area(ax, seq, bands, 'FaceAlpha', 1);
|
||||
xlim(ax, [seq(1), seq(end)]);
|
||||
xlabel(ax, D.xlabel);
|
||||
ylabel(ax, D.ylabel);
|
||||
legend(ax, h, D.legend, 'Location', 'northwest');
|
||||
fig_save(fig, S, 'timing-measured-q3-10-sources.png');
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
function D = load_data(S, name)
|
||||
%LOAD_DATA 读取 scripts/build_figure_data.py 预先写好的绘图数据并规范化文本字段。
|
||||
path = fullfile(S.data_dir, [name '.mat']);
|
||||
if ~isfile(path)
|
||||
error('缺少绘图数据:%s(请先运行 scripts/build_figure_data.py --prepare)', path);
|
||||
end
|
||||
D = normalize_text(load(path));
|
||||
if isfield(D, 'switch_') && ~isfield(D, 'switch_s')
|
||||
D.switch_s = D.switch_;
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
function D = normalize_text(D)
|
||||
%NORMALIZE_TEXT 把 load 出来的字符矩阵还原成字符串列表。
|
||||
% scipy.io.savemat 会把 Python 的字符串列表写成三维 char 数组(或 char 矩阵),
|
||||
% MATLAB 侧读回来是 char,不能按 {i} 取值,这里统一转成 cellstr。
|
||||
|
||||
fn = fieldnames(D);
|
||||
for k = 1:numel(fn)
|
||||
v = D.(fn{k});
|
||||
if ischar(v) && ndims(v) > 2
|
||||
D.(fn{k}) = cellstr(v);
|
||||
elseif isstruct(v)
|
||||
D.(fn{k}) = normalize_text(v);
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
function c = pick_color(S, name)
|
||||
%PICK_COLOR 语义色名 → MATLAB 默认配色(不引入自定义调色板)。
|
||||
switch name
|
||||
case 'primary', c = S.c(1, :); % 几何区域、主体对象
|
||||
case 'accent', c = S.c(2, :); % 真值源、关键结论
|
||||
case 'warn', c = S.c(3, :); % 警示、未清除
|
||||
case 'violet', c = S.c(4, :); % 直径圆等辅助对象
|
||||
case 'success', c = S.c(5, :); % 覆盖、清除、通过
|
||||
case 'grid', c = [0.5 0.5 0.5];
|
||||
case 'muted', c = [0.45 0.45 0.45];
|
||||
otherwise, c = [0 0 0];
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
function run_all_figures()
|
||||
%RUN_ALL_FIGURES 生成 docs/figures 下的全部论文插图。
|
||||
% 数据由 scripts/build_figure_data.py 写入 docs/figures/.data/*.mat。
|
||||
% 本机图形子系统在同一批处理会话里连续渲染多张图会崩溃,故每张图单独起会话;
|
||||
% 这里只作为 MATLAB 内的手动入口,逐张调用并记日志。
|
||||
|
||||
here = fileparts(mfilename('fullpath'));
|
||||
addpath(here);
|
||||
log = fullfile(here, 'run_all_figures.log');
|
||||
items = { 'flow-overall-architecture', @fig_flow_overall
|
||||
'steps-state-machine', @fig_steps_state_machine
|
||||
'timing-attachment-example-105-111-194-199', @fig_timing_attachment
|
||||
'timing-measured-q3-10-sources', @fig_timing_measured
|
||||
'geo-q1-convex-intersection-zoom', @fig_geo_q1_meetup
|
||||
'geo-q1-diameter-circle-counterexample', @fig_geo_q1_counterexample
|
||||
'geo-q2-guaranteed-reception-disk', @fig_geo_q2_safe_disk
|
||||
'geo-q3-seven-point-coverage', @fig_geo_q3_coverage
|
||||
'geo-q4-minimum-cover-31-points', @fig_geo_q4_mincover
|
||||
'replay-webui-q3-omnidirectional', @() fig_replay('replay_q3')
|
||||
'replay-webui-q4-directional', @() fig_replay('replay_q4') };
|
||||
fid = fopen(log, 'w');
|
||||
failed = 0;
|
||||
for i = 1:2:numel(items)
|
||||
name = items{i};
|
||||
fn = items{i + 1};
|
||||
try
|
||||
fn();
|
||||
fprintf(fid, 'OK %s\n', name);
|
||||
catch err
|
||||
failed = failed + 1;
|
||||
fprintf(fid, 'FAIL %s: %s\n', name, err.message);
|
||||
end
|
||||
end
|
||||
fclose(fid);
|
||||
if failed > 0
|
||||
fprintf('有 %d 张图生成失败,详见 %s\n', failed, log);
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user