Development Workflows - ShadobaDev/Platemaker-qt GitHub Wiki
What happens, step by step, when a workspace is opened or created, a project is opened, a render runs or is cancelled, templates are generated, and canvas profiles are managed.
File → Open workspace… (or Open recent ▸)
→ QFileDialog (*.platemaker.json)
→ loadWorkspace(path):
take the folder's WorkspaceLock // in use elsewhere? → OK / Take over / Cancel
more than one *.platemaker.json there? → resolve wizard (keep one, the rest to the
Recycle Bin), or cancel and keep what is open
WorkspaceSerializer::load(path)
register the workspace's own fonts/
read the authoring records back from the overlay SVGs (Svg::readOverlays())
populate the workspace panel (project list)
→ next event-loop pass: repair notice, stale-canvas warning,
then, with the folder held: healFontFallbacks(), sweepWorkspaceFolder()
See Workspace folder ownership for the lock, the fonts, the heal and the sweep.
File → New workspace… (Save as… goes through the same folder check)
→ QFileDialog (choose save location)
→ folder already holds another workspace? → refused: create a subfolder named after the file,
choose another location, or cancel
→ an empty Workspace — no output profiles of its own; the code-defined presets are always
available, and the first one is the active profile
→ WorkspaceSerializer::save(path)
→ open workspace panel
Workspace panel → double-click project OR right-click the list → New
→ Project dock opens (tabbed with the workspace panel), or is raised if already open
→ ProjectEditor::mergeFileScan() populates the tile grid from the input directory
→ thumbnails load asynchronously in background
The pipeline runs on a RenderWorker moved to its own QThread; it holds copies of the inputs,
profiles and output dir, so it never touches the live workspace. The lib reports progress through a
Core::ProcessingCallbacks struct (plain std::functions called synchronously on the worker thread);
each callback lambda does one cheap thing — emit a Qt signal — which is delivered to the main thread
via a queued connection, so the reaction (repainting tiles) happens on the main thread while the
render is never blocked on the UI.
Render → Render current project (F5) (or the project's Render button)
→ startRender(projectIndex):
askRenderGate() // an Error advisory? → its action / its resolution / Render anyway / Cancel
project.sanitize(workspace.canvasProfiles()) // refresh statuses (disk + config)
if up-to-date and no config change → inform user, skip
confirm if outputs are stale (format/size/canvas changed since last render)
worker = new RenderWorker(copies…); setColourCorrection(), setStripOverlays()
worker.moveToThread(thread)
connect worker → MainWindow: progress, log,
sliceSaved(index,…) → setOutputTile(index,…) // live, positional
inputStatus(path,…) → setInputTileStatus(path,…) // live, phase 1
ProcessingPipeline::run(inputs, outProfile, canvasProfiles, canvasProfileIds,
outDir, cancel, callbacks, onlySlices?, cacheDir,
colourCorrection, stripOverlays) // static; worker thread
show progress bar + Stop button
→ onRenderFinished():
project.applyProcessingResults(records, appliedProfiles, outcome.skippedPages,
workspace.canvasProfiles(), outDir, timestamp)
// skipped pages are recorded as FileStatus::Skipped, not Processed
deleteOrphanedOutputs() // files the new config no longer produces
populate() + WorkspaceSerializer::save()
During phase 1 (strip building) each input's tile turns green as it is appended, cyan Processed (no canvas profile) when it is rendered without a matching profile, or violet Skipped when it is left out (missing / load error); then output tiles stream in per slice. See The image tile in Main window. The render gate is described in Inputs and anchoring.
The two optional steps ride along. startRender copies the project's colourCorrection and
getStripOverlays() into the worker; both default to empty, so a project using neither renders
byte-identically to one built before they existed. They also need a staleness axis of their own: a grade
tweak or a moved bubble changes no input and no output file, so Models::processingConfigSignature() is
compared against the stored ProjectItem::processingSignature and a mismatch folds into the
"needs a full re-render" decision, exactly as outputProfileSignature() does for format/size changes.
The signature is stamped back after a completed render.
Render output contract (consumer side — the library's Render page). startRender passes the workspace's
.platemaker-cache dir to run(), so the pipeline warms each slice's thumbnail from its in-RAM pixels
before sliceSaved fires. The output tile's getOrGenerate() is then a cache hit that never
re-reads a slice the render is still writing — on Windows, reading a slice while it is being written
aborts the render with unable to open for write. Rules the GUI must keep: treat sliceSaved(path) as the only
"ready" signal (never read an output before it or during a run that rewrites it); a locked output surfaces
as ProcessingErrorCode::OutputLocked and is shown as a short Render failed — see the action log status
(the lib does not retry — that policy is ours).
Stop (Esc), or the project's Render button while a render runs
→ cancelRender(): m_cancelToken.cancel(), "Cancelling…", Stop disabled
→ the pipeline checks the token between slices and exits early
→ onRenderFinished(): the slices already written are applied as partial results,
so their hashes and statuses are current; "Render cancelled (partial output kept)."
Canvas → Templates… (TemplatesDialog) OR ManageCanvasProfilesDialog → generate for the selected profile
→ a profile not saved yet (no id)? → "save the new profile first"
→ TemplatesDialog::confirmOverwrite() // an existing template is replaced only on a yes
→ TemplatesDialog::generateTemplate():
TemplateGenerator::generate(profile, outputProfile, <workspace>/templates/<profile>.psd|.png,
TemplateGenerator::optionsFor(profile)) // dpi, format, empty layer
// the output profile is required by the signature and unused
→ WorkspaceEditor::setCanvasProfileTemplateInfo(id, templateInfo) // path + fingerprint
The format combo and empty-layer box under the table write the selected profile's
templateSettings through WorkspaceEditor::replaceCanvasProfiles(); the whole dialog is one undo
step (Edit templates).
Canvas → Open templates folder opens templates/.
Canvas → Manage profiles…
→ ManageCanvasProfilesDialog opens on a COPY of workspace.canvasProfiles()
→ Add / Edit / Delete happen on that copy inside the dialog
→ OK → WorkspaceEditor(m_workspace).replaceCanvasProfiles(copy)
// mints ids for new profiles, dedups, carries templateInfo by id
setDirty(true); emit workspaceProfilesChanged() // open projects refresh live
The output-profile equivalent is the same through replaceOutputProfiles() (which additionally strips
any preset — presets are code-defined and never persisted). Single-profile edits (edit-active,
double-click) use a copy → mutate → replace of the whole palette, since the palette is private and
cannot be mutated in place.
Conflict guard (the library's Project page):
Linking a canvas profile to a project goes through
WorkspaceEditor::addCanvasProfileToProject(), which returns false when the profile's canvas W×H
collides with one already linked; the GUI shows an error and does not link it. The project's
canvasProfileIds are private in the model, so a raw bypass of this guard is not possible.
See also: Main window · Rendering · Canvas profiles