Move project save write planning to app core

This commit is contained in:
2026-06-06 12:00:57 +02:00
parent ed9709ade8
commit a03db82307
8 changed files with 199 additions and 15 deletions

View File

@@ -301,8 +301,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
retained project-save target paths, including the requested PPI path, retained project-save target paths, including the requested PPI path,
temporary `.tmp.ppi` path, and timelapse `.pptl` sidecar path. The live temporary `.tmp.ppi` path, and timelapse `.pptl` sidecar path. The live
`Canvas::project_save_thread` consumes the same planner before retained `Canvas::project_save_thread` consumes the same planner before retained
serialization, and the command is covered by forward-slash, Windows serialization. The command also reports the app-core write-mode plan for
backslash, and invalid-path smokes. direct first saves versus existing-target temporary writes, including the
retained direct-write fallback when the temporary file cannot be opened, and
it is covered by forward-slash, Windows backslash, existing-target, and
invalid-path smokes.
- Live equirectangular, layer, animation-frame, and cube-face export adapters - Live equirectangular, layer, animation-frame, and cube-face export adapters
now prepare and log the same payload-bearing canvas document snapshot plus now prepare and log the same payload-bearing canvas document snapshot plus
shared paint-renderer export-readiness report. shared paint-renderer export-readiness report.
@@ -1182,7 +1185,8 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p
prepare and log a payload-bearing canvas document snapshot report, run the prepare and log a payload-bearing canvas document snapshot report, run the
app-core pure PPI save-writer route for payload-complete snapshots, log app-core pure PPI save-writer route for payload-complete snapshots, log
generated byte counts, and derive project-save target/tmp/timelapse paths generated byte counts, and derive project-save target/tmp/timelapse paths
through `pp_app_core` before delegating to retained `Canvas::project_save`. plus direct-vs-temporary write-mode decisions through `pp_app_core` before
delegating to retained `Canvas::project_save`.
Retained legacy UI/canvas Retained legacy UI/canvas
execution and actual live save serialization remain tracked by `DEBT-0040`, execution and actual live save serialization remain tracked by `DEBT-0040`,
`DEBT-0041`, and `DEBT-0042`; the pure snapshot-to-PPI export handoff is `DEBT-0041`, and `DEBT-0042`; the pure snapshot-to-PPI export handoff is

View File

@@ -541,6 +541,14 @@ agent or engineer to remove them without reconstructing context from chat.
`pano_cli plan-canvas-project-save-target` exposes it for automation. Actual `pano_cli plan-canvas-project-save-target` exposes it for automation. Actual
PPI serialization, temporary-file swap execution, progress/threading, PPI serialization, temporary-file swap execution, progress/threading,
timelapse sidecar serialization, and app metadata mutation remain retained. timelapse sidecar serialization, and app metadata mutation remain retained.
- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. The same app-core
project-save planner now owns the direct-write versus temporary-write mode
decision for new and existing targets, including the retained compatibility
fallback to direct target writes when the temporary file cannot be opened.
`Canvas::project_save_thread` consumes that policy before retained
serialization, and `pano_cli plan-canvas-project-save-target` reports it in
JSON. Actual PPI bytes, temporary-file swap execution, progress/threading,
timelapse sidecar serialization, and app metadata mutation remain retained.
- 2026-06-05: DEBT-0010/DEBT-0013 were narrowed again. `pp_app_core` now - 2026-06-05: DEBT-0010/DEBT-0013 were narrowed again. `pp_app_core` now
exports payload-complete or metadata-only canvas document snapshots through exports payload-complete or metadata-only canvas document snapshots through
the pure `pp_document` PPI writer and rejects snapshots that still require the pure `pp_document` PPI writer and rejects snapshots that still require

View File

@@ -709,7 +709,11 @@ save-writer route JSON. `pp_app_core` also owns the retained project-save target
path planner for target, temporary PPI, and timelapse sidecar paths; live path planner for target, temporary PPI, and timelapse sidecar paths; live
`Canvas::project_save_thread` consumes that planner and `Canvas::project_save_thread` consumes that planner and
`pano_cli plan-canvas-project-save-target` exposes it for automation. The same `pano_cli plan-canvas-project-save-target` exposes it for automation. The same
automation now feeds payload-complete snapshots through the shared app-core boundary now also plans the retained save write mode, distinguishing
direct first saves from existing-target temporary writes that swap into place
and preserving the legacy direct-write fallback when the temporary file cannot
be opened. The same automation now feeds payload-complete snapshots through the
shared
`pp_paint_renderer::prepare_document_frame_export_readiness` report, which `pp_paint_renderer::prepare_document_frame_export_readiness` report, which
records renderer-neutral six-face texture upload commands and encodes the records renderer-neutral six-face texture upload commands and encodes the
active document frame's six composited faces to PNG bytes. This gives CLI active document frame's six composited faces to PNG bytes. This gives CLI
@@ -2313,6 +2317,9 @@ Results:
temporary `.tmp.ppi` path, and timelapse `.pptl` sidecar. The live temporary `.tmp.ppi` path, and timelapse `.pptl` sidecar. The live
`Canvas::project_save_thread` consumes the same planner before retained `Canvas::project_save_thread` consumes the same planner before retained
serialization, reducing inline path compatibility logic in the legacy writer. serialization, reducing inline path compatibility logic in the legacy writer.
It also reports the app-core write-mode plan for direct first saves versus
existing-target temporary writes, including the retained fallback to direct
target writes when the temporary file cannot be opened.
- The same payload-complete snapshot automation now uploads the active document - The same payload-complete snapshot automation now uploads the active document
frame through `pp_paint_renderer::upload_document_frame_faces` and the frame through `pp_paint_renderer::upload_document_frame_faces` and the
`RecordingRenderDevice`, emitting `rendererUpload` JSON with texture, `RecordingRenderDevice`, emitting `rendererUpload` JSON with texture,

View File

@@ -103,6 +103,21 @@ struct DocumentCanvasProjectSaveTargetPlan {
std::string timelapse_path; std::string timelapse_path;
}; };
enum class DocumentCanvasProjectSaveWriteAction {
write_direct_to_target,
write_temporary_then_swap,
};
struct DocumentCanvasProjectSaveWritePlan {
DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target;
std::string write_path;
std::string target_path;
std::string temporary_path;
bool target_exists = false;
bool uses_temporary = false;
bool falls_back_to_direct_on_temporary_open_failure = false;
};
class DocumentCanvasClearServices { class DocumentCanvasClearServices {
public: public:
virtual ~DocumentCanvasClearServices() = default; virtual ~DocumentCanvasClearServices() = default;
@@ -350,6 +365,38 @@ plan_document_canvas_project_save_target(
return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan)); return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan));
} }
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>
plan_document_canvas_project_save_write(
const DocumentCanvasProjectSaveTargetPlan& target,
bool target_exists)
{
if (target.target_path.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
pp::foundation::Status::invalid_argument("project save write target path must not be empty"));
}
DocumentCanvasProjectSaveWritePlan plan;
plan.target_exists = target_exists;
plan.target_path = target.target_path;
plan.temporary_path = target.temporary_path;
if (!target_exists) {
plan.write_path = target.target_path;
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
}
if (target.temporary_path.empty()) {
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
pp::foundation::Status::invalid_argument("project save temporary path must not be empty"));
}
plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap;
plan.write_path = target.temporary_path;
plan.uses_temporary = true;
plan.falls_back_to_direct_on_temporary_open_failure = true;
return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear( [[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
bool has_canvas, bool has_canvas,
float r = 0.0F, float r = 0.0F,

View File

@@ -2370,7 +2370,7 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress)
// static char name[128]; // static char name[128];
// sprintf(name, "%s/latlong.ppi", data_path.c_str()); // sprintf(name, "%s/latlong.ppi", data_path.c_str());
FILE* fp; FILE* fp = nullptr;
const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path); const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path);
if (!save_target) { if (!save_target) {
@@ -2385,15 +2385,25 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress)
LOG("file name %s", file_name.c_str()); LOG("file name %s", file_name.c_str());
LOG("tmp path %s", tmp_path.c_str()); LOG("tmp path %s", tmp_path.c_str());
bool use_tmp = false; bool target_exists = false;
// check if file already exists if ((fp = fopen(file_path.c_str(), "rb"))) {
if ((fp = fopen(file_path.c_str(), "rb")))
{
fclose(fp); fclose(fp);
fp = nullptr;
target_exists = true;
}
const auto write_plan = pp::app::plan_document_canvas_project_save_write(save_paths, target_exists);
if (!write_plan) {
LOG("cannot plan project save write for %s: %s", file_path.c_str(), write_plan.status().message);
return false;
}
bool use_tmp = write_plan.value().uses_temporary;
if (write_plan.value().uses_temporary)
{
LOG("use tmp file"); LOG("use tmp file");
// use tmp file for writing fp = fopen(write_plan.value().write_path.c_str(), "wb");
use_tmp = true; if (!fp)
if (!(fp = fopen(tmp_path.c_str(), "wb")))
{ {
LOG("cannot write tmp project to %s", tmp_path.c_str()); LOG("cannot write tmp project to %s", tmp_path.c_str());
use_tmp = false; use_tmp = false;

View File

@@ -1530,7 +1530,7 @@ if(TARGET pano_cli)
COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi) COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi)
set_tests_properties(pano_cli_plan_canvas_project_save_target_smoke PROPERTIES set_tests_properties(pano_cli_plan_canvas_project_save_target_smoke PROPERTIES
LABELS "app;document;integration;desktop-fast" LABELS "app;document;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"dataDirectory\":\"D:/Paint/data\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"dataDirectory\":\"D:/Paint/data\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\".*\"writePlan\":\\{\"action\":\"write-direct-to-target\",\"targetExists\":false,\"usesTemporary\":false,\"writePath\":\"D:/Paint/projects/demo.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":false\\}")
add_test(NAME pano_cli_plan_canvas_project_save_target_backslash_smoke add_test(NAME pano_cli_plan_canvas_project_save_target_backslash_smoke
COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path "D:\\Paint\\projects\\demo.ppi") COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path "D:\\Paint\\projects\\demo.ppi")
@@ -1538,6 +1538,12 @@ if(TARGET pano_cli)
LABELS "app;document;integration;desktop-fast" LABELS "app;document;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\"") PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"fileName\":\"demo\".*\"temporaryPath\":\"D:/Paint/data/demo.tmp.ppi\".*\"timelapsePath\":\"D:/Paint/data/demo.pptl\"")
add_test(NAME pano_cli_plan_canvas_project_save_target_existing_smoke
COMMAND pano_cli plan-canvas-project-save-target --data-dir D:/Paint/data --path D:/Paint/projects/demo.ppi --target-exists)
set_tests_properties(pano_cli_plan_canvas_project_save_target_existing_smoke PROPERTIES
LABELS "app;document;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-project-save-target\".*\"targetPath\":\"D:/Paint/projects/demo.ppi\".*\"writePlan\":\\{\"action\":\"write-temporary-then-swap\",\"targetExists\":true,\"usesTemporary\":true,\"writePath\":\"D:/Paint/data/demo.tmp.ppi\",\"fallbackDirectOnTemporaryOpenFailure\":true\\}")
add_test(NAME pano_cli_plan_canvas_project_save_target_rejects_empty_path add_test(NAME pano_cli_plan_canvas_project_save_target_rejects_empty_path
COMMAND pano_cli plan-canvas-project-save-target --path "") COMMAND pano_cli plan-canvas-project-save-target --path "")
set_tests_properties(pano_cli_plan_canvas_project_save_target_rejects_empty_path PROPERTIES set_tests_properties(pano_cli_plan_canvas_project_save_target_rejects_empty_path PROPERTIES

View File

@@ -287,6 +287,72 @@ void project_save_target_plan_rejects_empty_inputs(pp::tests::Harness& harness)
PP_EXPECT(harness, no_name.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, no_name.status().code == pp::foundation::StatusCode::invalid_argument);
} }
void project_save_write_plan_writes_direct_for_new_targets(pp::tests::Harness& harness)
{
const auto target = pp::app::plan_document_canvas_project_save_target(
"D:/Paint/data",
"D:/Paint/projects/demo.ppi");
PP_EXPECT(harness, target);
if (!target) {
return;
}
const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), false);
PP_EXPECT(harness, plan);
if (!plan) {
return;
}
PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_direct_to_target);
PP_EXPECT(harness, plan.value().write_path == "D:/Paint/projects/demo.ppi");
PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi");
PP_EXPECT(harness, !plan.value().target_exists);
PP_EXPECT(harness, !plan.value().uses_temporary);
PP_EXPECT(harness, !plan.value().falls_back_to_direct_on_temporary_open_failure);
}
void project_save_write_plan_prefers_temporary_for_existing_targets(pp::tests::Harness& harness)
{
const auto target = pp::app::plan_document_canvas_project_save_target(
"D:/Paint/data",
"D:/Paint/projects/demo.ppi");
PP_EXPECT(harness, target);
if (!target) {
return;
}
const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), true);
PP_EXPECT(harness, plan);
if (!plan) {
return;
}
PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap);
PP_EXPECT(harness, plan.value().write_path == "D:/Paint/data/demo.tmp.ppi");
PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi");
PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi");
PP_EXPECT(harness, plan.value().target_exists);
PP_EXPECT(harness, plan.value().uses_temporary);
PP_EXPECT(harness, plan.value().falls_back_to_direct_on_temporary_open_failure);
}
void project_save_write_plan_rejects_missing_paths(pp::tests::Harness& harness)
{
pp::app::DocumentCanvasProjectSaveTargetPlan empty_target;
const auto no_target = pp::app::plan_document_canvas_project_save_write(empty_target, false);
pp::app::DocumentCanvasProjectSaveTargetPlan no_temporary;
no_temporary.target_path = "D:/Paint/projects/demo.ppi";
const auto missing_temporary = pp::app::plan_document_canvas_project_save_write(no_temporary, true);
PP_EXPECT(harness, !no_target);
PP_EXPECT(harness, !missing_temporary);
PP_EXPECT(harness, no_target.status().code == pp::foundation::StatusCode::invalid_argument);
PP_EXPECT(harness, missing_temporary.status().code == pp::foundation::StatusCode::invalid_argument);
}
void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness)
{ {
const std::uint32_t frames[] { 100U }; const std::uint32_t frames[] { 100U };
@@ -471,6 +537,9 @@ int main()
harness.run("project save target plan preserves legacy paths", project_save_target_plan_preserves_legacy_paths); harness.run("project save target plan preserves legacy paths", project_save_target_plan_preserves_legacy_paths);
harness.run("project save target plan accepts windows backslashes", project_save_target_plan_accepts_windows_backslashes); harness.run("project save target plan accepts windows backslashes", project_save_target_plan_accepts_windows_backslashes);
harness.run("project save target plan rejects empty inputs", project_save_target_plan_rejects_empty_inputs); harness.run("project save target plan rejects empty inputs", project_save_target_plan_rejects_empty_inputs);
harness.run("project save write plan writes direct for new targets", project_save_write_plan_writes_direct_for_new_targets);
harness.run("project save write plan prefers temporary for existing targets", project_save_write_plan_prefers_temporary_for_existing_targets);
harness.run("project save write plan rejects missing paths", project_save_write_plan_rejects_missing_paths);
harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state); harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state);
harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects); harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects);
harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas); harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas);

View File

@@ -413,6 +413,7 @@ struct PlanCanvasClearArgs {
struct PlanCanvasProjectSaveTargetArgs { struct PlanCanvasProjectSaveTargetArgs {
std::string data_directory = "D:/Paint/data"; std::string data_directory = "D:/Paint/data";
std::string target_path = "D:/Paint/projects/demo.ppi"; std::string target_path = "D:/Paint/projects/demo.ppi";
bool target_exists = false;
}; };
struct PlanCanvasDocumentSnapshotArgs { struct PlanCanvasDocumentSnapshotArgs {
@@ -966,6 +967,19 @@ const char* document_canvas_save_writer_action_name(pp::app::DocumentCanvasSaveW
return "use-legacy-project-save"; return "use-legacy-project-save";
} }
const char* document_canvas_project_save_write_action_name(
pp::app::DocumentCanvasProjectSaveWriteAction action) noexcept
{
switch (action) {
case pp::app::DocumentCanvasProjectSaveWriteAction::write_direct_to_target:
return "write-direct-to-target";
case pp::app::DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap:
return "write-temporary-then-swap";
}
return "write-direct-to-target";
}
const char* document_workflow_decision_name(pp::app::DocumentWorkflowDecision decision) noexcept const char* document_workflow_decision_name(pp::app::DocumentWorkflowDecision decision) noexcept
{ {
switch (decision) { switch (decision) {
@@ -2546,7 +2560,7 @@ void print_help()
<< " plan-canvas-view-density [--density N] [--bad-float]\n" << " plan-canvas-view-density [--density N] [--bad-float]\n"
<< " plan-canvas-view-cursor-mode [--mode N]\n" << " plan-canvas-view-cursor-mode [--mode N]\n"
<< " plan-canvas-cursor [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--visibility never|small-brush|not-painting|always] [--brush-size N] [--no-brush] [--drawing] [--alt] [--resizing] [--picking] [--bad-size]\n" << " plan-canvas-cursor [--mode draw|erase|line|camera|grid|copy|cut|fill|mask-free|mask-line|bucket] [--visibility never|small-brush|not-painting|always] [--brush-size N] [--no-brush] [--drawing] [--alt] [--resizing] [--picking] [--bad-size]\n"
<< " plan-canvas-project-save-target [--data-dir DIR] [--path FILE]\n" << " plan-canvas-project-save-target [--data-dir DIR] [--path FILE] [--target-exists]\n"
<< " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n" << " plan-grid-operation --kind pick|load|reload|clear|render|commit [--path FILE] [--no-heightmap] [--no-canvas] [--float32] [--float16] [--texture-resolution N] [--samples N]\n"
<< " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n" << " plan-history-operation --kind undo|redo|clear [--undo-count N] [--redo-count N] [--memory-bytes N]\n"
<< " plan-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\n" << " plan-main-toolbar --command open|save|undo|redo|clear-history|clear-canvas|message-box|settings [--undo-count N] [--redo-count N] [--memory-bytes N] [--no-canvas]\n"
@@ -6075,6 +6089,8 @@ pp::foundation::Status parse_plan_canvas_project_save_target_args(
} else { } else {
args.target_path = argv[++i]; args.target_path = argv[++i];
} }
} else if (key == "--target-exists") {
args.target_exists = true;
} else { } else {
return pp::foundation::Status::invalid_argument("unknown option"); return pp::foundation::Status::invalid_argument("unknown option");
} }
@@ -6100,14 +6116,31 @@ int plan_canvas_project_save_target(int argc, char** argv)
return 2; return 2;
} }
const auto write_plan = pp::app::plan_document_canvas_project_save_write(
plan.value(),
args.target_exists);
if (!write_plan) {
print_error("plan-canvas-project-save-target", write_plan.status().message);
return 2;
}
const auto& value = plan.value(); const auto& value = plan.value();
const auto& write_value = write_plan.value();
std::cout << "{\"ok\":true,\"command\":\"plan-canvas-project-save-target\"" std::cout << "{\"ok\":true,\"command\":\"plan-canvas-project-save-target\""
<< ",\"dataDirectory\":\"" << json_escape(args.data_directory) << ",\"dataDirectory\":\"" << json_escape(args.data_directory)
<< "\",\"targetPath\":\"" << json_escape(value.target_path) << "\",\"targetPath\":\"" << json_escape(value.target_path)
<< "\",\"fileName\":\"" << json_escape(value.file_name) << "\",\"fileName\":\"" << json_escape(value.file_name)
<< "\",\"temporaryPath\":\"" << json_escape(value.temporary_path) << "\",\"temporaryPath\":\"" << json_escape(value.temporary_path)
<< "\",\"timelapsePath\":\"" << json_escape(value.timelapse_path) << "\",\"timelapsePath\":\"" << json_escape(value.timelapse_path)
<< "\"}\n"; << "\",\"writePlan\":{\"action\":\""
<< document_canvas_project_save_write_action_name(write_value.action)
<< "\",\"targetExists\":" << json_bool(write_value.target_exists)
<< ",\"usesTemporary\":" << json_bool(write_value.uses_temporary)
<< ",\"writePath\":\"" << json_escape(write_value.write_path)
<< "\",\"fallbackDirectOnTemporaryOpenFailure\":"
<< json_bool(write_value.falls_back_to_direct_on_temporary_open_failure)
<< "}"
<< "}\n";
return 0; return 0;
} }