Skip to content

Commit 7edd619

Browse files
committed
fix: normalize figure studio fig imports
1 parent b9cd4da commit 7edd619

10 files changed

Lines changed: 181 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ maintainer intent and user impact that are easy to lose in commit subjects.
2424
Affected versions:
2525
- `labkit_launcher` `1.2.3 -> 1.2.4`
2626
- `labkit.ui` `4.2.0 -> 4.2.1`
27-
- `labkit_FigureStudio_app` `0.1.0 -> 0.1.1`
27+
- `labkit_FigureStudio_app` `0.1.0 -> 0.1.2`
2828

2929
What changed:
3030
- Added visible busy/progress feedback for launcher actions that can wait on
@@ -41,6 +41,9 @@ What changed:
4141
switching between the measured LabKit single-panel style and the imported
4242
FIG defaults, tuning font/line/grid parameters, and exporting visible
4343
graphics data packages with reconstruction scripts.
44+
- Normalized imported FIG axes before applying Studio canvas and style
45+
constraints so source layout/aspect metadata and file-selection titles cannot
46+
collapse the preview.
4447

4548
Why it matters:
4649
- Users can distinguish long launcher work from a frozen MATLAB session.
@@ -89,7 +92,7 @@ on 2026-07-06.
8992
| `labkit.thermal` | `1.0.0` | Facade | `+labkit/+thermal/version.m` |
9093
| `labkit.rhs` | `1.0.0` | Facade | `+labkit/+rhs/version.m` |
9194
| `labkit.biosignal` | `1.0.0` | Facade | `+labkit/+biosignal/version.m` |
92-
| `labkit_FigureStudio_app` | `0.1.1` | LabKit Core | `apps/labkit_core/figure_studio/+figure_studio/version.m` |
95+
| `labkit_FigureStudio_app` | `0.1.2` | LabKit Core | `apps/labkit_core/figure_studio/+figure_studio/version.m` |
9396
| `labkit_ChronoOverlay_app` | `1.3.3` | Electrochem | `apps/electrochem/chrono_overlay/+chrono_overlay/version.m` |
9497
| `labkit_CIC_app` | `1.3.5` | Electrochem | `apps/electrochem/cic/+cic/version.m` |
9598
| `labkit_CSC_app` | `1.3.7` | Electrochem | `apps/electrochem/csc/+csc/version.m` |

apps/labkit_core/figure_studio/+figure_studio/+resultFiles/applyFigureStyle.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,14 @@ function applyAxesCanvasFrame(ax, width, height)
281281
return;
282282
end
283283
try
284+
drawnow;
284285
parentPixels = getpixelposition(parent, true);
285286
margin = 24;
286287
availableWidth = max(1, parentPixels(3) - 2 * margin);
287288
availableHeight = max(1, parentPixels(4) - 2 * margin);
289+
if availableWidth < 240 || availableHeight < 180
290+
return;
291+
end
288292
scale = min(1, min(availableWidth / width, availableHeight / height));
289293
frameWidth = max(1, round(width * scale));
290294
frameHeight = max(1, round(height * scale));

apps/labkit_core/figure_studio/+figure_studio/+sourceAxes/copyToPreview.m

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function copyToPreview(srcAx, dstAx)
1414
xlabel(dstAx, string(srcAx.XLabel.String), 'Interpreter', 'none');
1515
ylabel(dstAx, string(srcAx.YLabel.String), 'Interpreter', 'none');
1616
zlabel(dstAx, string(srcAx.ZLabel.String), 'Interpreter', 'none');
17+
normalizePreviewAxesLayout(dstAx);
1718
labkit.ui.tool.enableAxesPopout(dstAx);
1819
end
1920

@@ -28,9 +29,7 @@ function disableDefaultAxesToolbar(ax)
2829
function copyAxesState(srcAx, dstAx)
2930
props = {'XScale','YScale','ZScale','XDir','YDir','ZDir', ...
3031
'XLim','YLim','ZLim','CLim','View','Box','XGrid','YGrid','ZGrid', ...
31-
'Color','XColor','YColor','ZColor','LineWidth','FontName','FontSize', ...
32-
'DataAspectRatio','DataAspectRatioMode', ...
33-
'PlotBoxAspectRatio','PlotBoxAspectRatioMode'};
32+
'Color','XColor','YColor','ZColor','LineWidth','FontName','FontSize'};
3433
for k = 1:numel(props)
3534
try
3635
dstAx.(props{k}) = srcAx.(props{k});
@@ -42,3 +41,13 @@ function copyAxesState(srcAx, dstAx)
4241
catch
4342
end
4443
end
44+
45+
function normalizePreviewAxesLayout(ax)
46+
props = {'DataAspectRatioMode', 'PlotBoxAspectRatioMode'};
47+
for k = 1:numel(props)
48+
try
49+
ax.(props{k}) = 'auto';
50+
catch
51+
end
52+
end
53+
end

apps/labkit_core/figure_studio/+figure_studio/+sourceAxes/importFigFile.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'The selected FIG file does not contain axes.');
1212
end
1313
srcAx = axesHandles(1);
14-
sourceStyle = figure_studio.sourceAxes.sourceStyle(srcAx);
14+
sourceStyle = figure_studio.sourceAxes.sourceStyle(srcAx, ...
15+
"PreserveAspect", false);
1516
figure_studio.sourceAxes.copyToPreview(srcAx, dstAx);
1617
end

apps/labkit_core/figure_studio/+figure_studio/+sourceAxes/sourceStyle.m

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
% Read style defaults from a source axes for Figure Studio. Expected caller is
22
% figure_studio.definitionActions; output follows the app-owned style struct.
3-
function style = sourceStyle(srcAx)
3+
function style = sourceStyle(srcAx, opts)
4+
arguments
5+
srcAx
6+
opts.PreserveAspect (1, 1) logical = true
7+
end
8+
49
style = figure_studio.styleLibrary.styleForPreset("FIG default");
510
style.name = "FIG default";
611
if isempty(srcAx) || ~isvalid(srcAx)
712
return;
813
end
9-
ratio = ratioFromVector(optionalAxesValue(srcAx, 'PlotBoxAspectRatio'));
10-
if ~isfinite(ratio)
14+
if opts.PreserveAspect
15+
ratio = ratioFromVector(optionalAxesValue(srcAx, 'PlotBoxAspectRatio'));
16+
if ~isfinite(ratio)
17+
ratio = ratioFromPosition(srcAx);
18+
end
19+
else
1120
ratio = ratioFromPosition(srcAx);
21+
if ~isfinite(ratio)
22+
ratio = ratioFromVector(optionalAxesValue(srcAx, 'PlotBoxAspectRatio'));
23+
end
1224
end
1325
width = 720;
1426
height = 540;

apps/labkit_core/figure_studio/+figure_studio/+userInterface/updateWorkbenchFromState.m

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ function updateWorkbenchFromState(state, ui, ~)
55
renderList(state, ui);
66
renderControls(state, ui);
77
renderPreviewStatus(state, ui);
8+
clearPreviewFileContextTitle(state, ui);
89
end
910

1011
function renderList(state, ui)
@@ -73,6 +74,23 @@ function renderEmptyPreview(state, ui)
7374
title(ax, "No figure loaded");
7475
end
7576

77+
function clearPreviewFileContextTitle(state, ui)
78+
if strlength(state.currentSource) == 0 || ...
79+
~isfield(ui.controls, 'preview') || ...
80+
~isfield(ui.controls.preview, 'axesById') || ...
81+
~isfield(ui.controls.preview.axesById, 'main')
82+
return;
83+
end
84+
ax = ui.controls.preview.axesById.main;
85+
try
86+
titleText = join(string(ax.Title.String), " ");
87+
if contains(titleText, " | file ") || startsWith(titleText, "file ")
88+
title(ax, "");
89+
end
90+
catch
91+
end
92+
end
93+
7694
function value = onOffText(tf)
7795
if tf
7896
value = 'On';

apps/labkit_core/figure_studio/+figure_studio/definitionActions.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ function pollStudioResize(timerObj, fig)
112112
setappdata(fig, key, pos);
113113
if hasPreviewContent(ax)
114114
figure_studio.resultFiles.applyFigureStyle(ax, previewStyle(runtime.state.style));
115+
clearFrameworkPreviewTitle(ax);
115116
end
116117
catch
117118
end
@@ -147,6 +148,7 @@ function onStudioResized(fig)
147148
ax = runtime.ui.controls.preview.axesById.main;
148149
if hasPreviewContent(ax)
149150
figure_studio.resultFiles.applyFigureStyle(ax, previewStyle(runtime.state.style));
151+
clearFrameworkPreviewTitle(ax);
150152
end
151153
catch
152154
end
@@ -263,6 +265,7 @@ function onStudioResized(fig)
263265
return;
264266
end
265267
figure_studio.resultFiles.applyFigureStyle(ax, previewStyle(state.style));
268+
clearFrameworkPreviewTitle(ax);
266269
state.status = "Styled with " + state.preset + ".";
267270
end
268271

@@ -362,6 +365,7 @@ function onStudioResized(fig)
362365
end
363366
try
364367
figure_studio.resultFiles.applyFigureStyle(ax, state.style);
368+
clearFrameworkPreviewTitle(ax);
365369
if format == "svg"
366370
exportgraphics(ax, filepath, "ContentType", "vector");
367371
else
@@ -372,13 +376,25 @@ function onStudioResized(fig)
372376
state.summary = summaryLines(state);
373377
addLog(services, state.status);
374378
figure_studio.resultFiles.applyFigureStyle(ax, previewStyle(state.style));
379+
clearFrameworkPreviewTitle(ax);
375380
catch ME
376381
figure_studio.resultFiles.applyFigureStyle(ax, previewStyle(state.style));
382+
clearFrameworkPreviewTitle(ax);
377383
reportException(services, "Quick export", ME);
378384
labkit.ui.app.showAlert(services.figure, ME.message, "Quick export");
379385
end
380386
end
381387

388+
function clearFrameworkPreviewTitle(ax)
389+
try
390+
titleText = join(string(ax.Title.String), " ");
391+
if contains(titleText, " | file ") || startsWith(titleText, "file ")
392+
title(ax, "");
393+
end
394+
catch
395+
end
396+
end
397+
382398
function state = adoptLaunchRequest(state, services)
383399
if ~isstruct(services) || ~isfield(services, 'request') || ...
384400
~isstruct(services.request) || ~isfield(services.request, 'launch')

apps/labkit_core/figure_studio/+figure_studio/version.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"name", "labkit_FigureStudio_app", ...
99
"displayName", "Figure Studio", ...
1010
"family", "LabKit Core", ...
11-
"version", "0.1.1", ...
11+
"version", "0.1.2", ...
1212
"updated", "2026-07-06");
1313
end

tests/cases/gui/apps/labkit_core/figure_studio/GuiLayoutFigureStudioTest.m

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function figure_studio_launches_with_style_library(testCase)
5959
driver.enabled('exportCurrent'), ...
6060
'Figure Studio should auto-open scanned FIG files into a styleable preview.');
6161
ax = driver.registry().controls.preview.axesById.main;
62+
assert(~contains(join(string(ax.Title.String), " "), " | file "), ...
63+
'Figure Studio should not style framework file-title context as plot content.');
6264
assert(isappdata(ax, 'labkitFigureStudioCanvasFrame'), ...
6365
'Figure Studio preview should track a fixed canvas frame.');
6466
frameBefore = getappdata(ax, 'labkitFigureStudioCanvasFrame');
@@ -87,6 +89,9 @@ function figure_studio_accepts_popout_axes_handoff(testCase)
8789
pbaspect(sourceAx, [2 1 1]);
8890

8991
fig = labkit_FigureStudio_app("axes", sourceAx);
92+
waitForNotBusy(fig);
93+
pause(1);
94+
drawnow;
9095
driver = labkitWorkflowDriver(fig);
9196
assert(driver.previewChildCount('preview') > 0 && ...
9297
driver.enabled('exportCurrent'), ...
@@ -99,6 +104,29 @@ function figure_studio_accepts_popout_axes_handoff(testCase)
99104
'Figure Studio axes handoff should preserve the source plot box ratio as a custom canvas.');
100105
end
101106

107+
function figure_studio_waits_for_stable_preview_canvas(testCase)
108+
setupLabKitTestPath();
109+
h = guiTestHelpers();
110+
h.assertUifigureAvailable();
111+
cleanup = onCleanup(@() h.closeAllFigures());
112+
113+
fig = uifigure('Visible', 'off', 'Position', [100 100 1200 800]);
114+
grid = uigridlayout(fig, [3 3]);
115+
ax = uiaxes(grid);
116+
ax.Layout.Row = 2;
117+
ax.Layout.Column = 2;
118+
plot(ax, linspace(0, 30, 200), sin(linspace(0, 30, 200)));
119+
120+
style = figure_studio.styleLibrary.styleForPreset("LabKit figure");
121+
style.previewScale = true;
122+
figure_studio.resultFiles.applyFigureStyle(ax, style);
123+
124+
frame = getappdata(ax, 'labkitFigureStudioCanvasFrame');
125+
assert(frame.scale > 0.5 && frame.position(3) > 0.5 && ...
126+
frame.position(4) > 0.5, ...
127+
'Figure Studio should not freeze preview canvas size from the initial 100x100 uigridlayout measurement.');
128+
end
129+
102130
function popout_send_to_studio_copies_plot_content(testCase)
103131
setupLabKitTestPath();
104132
h = guiTestHelpers();
@@ -120,8 +148,7 @@ function popout_send_to_studio_copies_plot_content(testCase)
120148
h.invokeCallback(studioTool(1), 'Callback');
121149
drawnow;
122150

123-
studioFig = findall(groot, 'Type', 'figure', 'Name', ...
124-
'Figure Studio v0.1.0 (2026-07-06)');
151+
studioFig = figureStudioFigures();
125152
assert(~isempty(studioFig), ...
126153
'Popout Studio handoff should launch Figure Studio.');
127154
driver = labkitWorkflowDriver(studioFig(1));
@@ -138,6 +165,15 @@ function removeStudioHook()
138165
end
139166
end
140167

168+
function figures = figureStudioFigures()
169+
allFigures = findall(groot, 'Type', 'figure');
170+
keep = false(size(allFigures));
171+
for k = 1:numel(allFigures)
172+
keep(k) = contains(string(allFigures(k).Name), "Figure Studio");
173+
end
174+
figures = allFigures(keep);
175+
end
176+
141177
function waitForNotBusy(fig)
142178
deadline = tic;
143179
while isvalid(fig) && isappdata(fig, 'labkitUiBusyDepth') && ...
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
classdef FigureStudioSourceAxesTest < matlab.unittest.TestCase
2+
%FIGURESTUDIOSOURCEAXESTEST Verify Figure Studio source axes import prep.
3+
4+
methods (Test, TestTags = {'Unit'})
5+
function copyToPreviewNormalizesLayoutBeforeStyling(testCase)
6+
setupLabKitTestPath();
7+
cleanup = onCleanup(@() closeAllTestFigures());
8+
9+
sourceFig = figure('Visible', 'off', 'Color', 'w');
10+
sourceAx = axes('Parent', sourceFig);
11+
plot(sourceAx, linspace(0, 30, 200), sin(linspace(0, 30, 200)));
12+
xlabel(sourceAx, 'Time');
13+
ylabel(sourceAx, 'Signal');
14+
pbaspect(sourceAx, [1 0.1 1]);
15+
daspect(sourceAx, [10 1 1]);
16+
17+
previewFig = figure('Visible', 'off', 'Color', 'w');
18+
previewAx = axes('Parent', previewFig);
19+
figure_studio.sourceAxes.copyToPreview(sourceAx, previewAx);
20+
21+
testCase.verifyEqual(string(previewAx.PlotBoxAspectRatioMode), "auto", ...
22+
"Imported FIG layout constraints should not be carried into the Studio preview.");
23+
testCase.verifyEqual(string(previewAx.DataAspectRatioMode), "auto", ...
24+
"Imported FIG data aspect constraints should be normalized before Studio styling.");
25+
testCase.verifyEqual(numel(previewAx.Children), numel(sourceAx.Children), ...
26+
"Imported FIG data graphics should still be copied into the preview.");
27+
end
28+
29+
function figFileImportUsesDisplayedAxesRatioForCanvas(testCase)
30+
setupLabKitTestPath();
31+
cleanup = onCleanup(@() closeAllTestFigures());
32+
33+
sourceFig = figure('Visible', 'off', 'Color', 'w', ...
34+
'Units', 'pixels', 'Position', [100 100 720 540]);
35+
sourceAx = axes('Parent', sourceFig, 'Units', 'normalized', ...
36+
'Position', [0.13 0.34 0.775 0.58]);
37+
plot(sourceAx, 1:4, [1 3 2 4]);
38+
pbaspect(sourceAx, [1 0.1 1]);
39+
40+
style = figure_studio.sourceAxes.sourceStyle(sourceAx, ...
41+
"PreserveAspect", false);
42+
ratio = double(style.canvasWidth) / double(style.canvasHeight);
43+
44+
testCase.verifyGreaterThan(ratio, 1.2, ...
45+
"FIG default canvas should follow displayed axes geometry.");
46+
testCase.verifyLessThan(ratio, 3, ...
47+
"FIG default canvas should not inherit extreme cached plot-box ratios.");
48+
end
49+
50+
function axesHandoffCanPreserveSourcePlotBoxRatio(testCase)
51+
setupLabKitTestPath();
52+
cleanup = onCleanup(@() closeAllTestFigures());
53+
54+
sourceFig = figure('Visible', 'off', 'Color', 'w');
55+
sourceAx = axes('Parent', sourceFig);
56+
plot(sourceAx, 1:4, [1 3 2 4]);
57+
pbaspect(sourceAx, [2 1 1]);
58+
59+
style = figure_studio.sourceAxes.sourceStyle(sourceAx);
60+
ratio = double(style.canvasWidth) / double(style.canvasHeight);
61+
62+
testCase.verifyLessThan(abs(ratio - 2), 0.02, ...
63+
"Axes handoff should still preserve explicit source plot-box ratios.");
64+
end
65+
end
66+
end
67+
68+
function closeAllTestFigures()
69+
delete(findall(groot, 'Type', 'figure'));
70+
end

0 commit comments

Comments
 (0)