Render in place: a track's output to a new sibling, source to the bench
This commit is contained in:
@@ -16,8 +16,10 @@ is owned by other directories and only skinned here.
|
||||
- **Ingest is an extension act; the instrument is a read-only bank consumer.** Any
|
||||
instrument code path that captures, imports, inserts a timeline item, or writes
|
||||
back into the bank is a bug — the instrument reads and plays only.
|
||||
- **`arrange_drop_win` is the only timeline-placing shell in this directory**, and
|
||||
it places because the USER dragged a card onto the arrange. Root `CLAUDE.md`'s
|
||||
- **`arrange_drop_win` is the only timeline-placing shell IN THIS DIRECTORY** — the
|
||||
claim scopes here, not to the system: `shell/capture` holds two more
|
||||
(`RunInsertSelected` and `render_in_place`, the third verb). `arrange_drop_win`
|
||||
places because the USER dragged a card onto the arrange. Root `CLAUDE.md`'s
|
||||
capture/placement separation forbids a CAPTURE placing an item; a deliberate drop
|
||||
is placement on demand. No other module here may grow an `InsertMedia` call.
|
||||
- **Ingest NEVER inserts a timeline item.** Arrange capture→bank→assign reuses the
|
||||
|
||||
@@ -45,9 +45,13 @@ detail not covered there:
|
||||
`capture_realtime_shell` cannot block REAPER's UI for the duration of a realtime
|
||||
record, so `begin`/`tick`/`abort` are async by construction and the temp-track +
|
||||
send recipe lives in the shell, not the pure core.
|
||||
- **`RunInsertSelected` is the one deliberate exception to capture-never-places**
|
||||
(see `capture_orchestrator` below) — every other capture entry point writes only
|
||||
a file + index entry.
|
||||
- **This directory hosts TWO placing paths, and neither is a capture placing
|
||||
itself.** `RunInsertSelected` (see `capture_orchestrator` below) places a *bank
|
||||
sample*, on demand, which is why it is the deliberate exception to
|
||||
capture-never-places. `render_in_place` places a render that never entered the
|
||||
bank — the third verb (arrange → arrange, root `CLAUDE.md` §The load-bearing
|
||||
principle). Every other entry point here writes only a file + index entry, and no
|
||||
capture may ever grow a place step.
|
||||
|
||||
## Modules
|
||||
|
||||
@@ -63,6 +67,7 @@ detail not covered there:
|
||||
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
|
||||
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
|
||||
- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`.
|
||||
- `render_in_place` (`shell/capture`) — the third verb, arrange → arrange: renders the selected track's output over the resolved range through `renderOffline` with `CaptureDestination::ProjectMedia`, then places the result on a brand-new sibling track at the render window's exact start (unsnapped — this placement IS the null test performed automatically), clones the source's colour and its name through the idempotent `captureTrackName`, and settles both tracks' modes in ONE `UNDO_STATE_ALL` block. Sibling nesting comes from the pure `core/capture/track_topology::siblingPlacement`. The source is tagged Design and the result track + its items are tagged `kArrangeModeId` **explicitly and unconditionally** — never `view.activeModeId()`, and never `untag()`, because the panel's auto-tag detector defers to a membership RECORD. It reads and writes NOTHING in the bank: no `session.bank()`, no `session.book()`, no `recordCreated`, no `bumpBankGeneration`; the `Sample` the backend returns is discarded and its `relativePath` is empty by construction. Traffic is one-way — capture may borrow this render, this placement may never be borrowed back into a capture.
|
||||
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.** The mono collapse needs no change here: `insert.cpp` passes only a path to `InsertMedia`, and REAPER derives the item's channel count from the file itself — a 1-channel WAV yields a mono item for free.
|
||||
- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively.
|
||||
- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys.
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetProjectPathEx
|
||||
#define REAPERAPI_WANT_GetSetProjectInfo
|
||||
#define REAPERAPI_WANT_GetSetProjectInfo_String
|
||||
#define REAPERAPI_WANT_GetSet_LoopTimeRange
|
||||
@@ -414,8 +415,29 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// Compute the tag ONCE — calling makeUniqueTag() twice would let the file
|
||||
// stem and Sample.id diverge (the counter advances per call).
|
||||
const std::string uniqueTag = makeUniqueTag("");
|
||||
const BankPaths paths =
|
||||
deriveBankPaths(projectDir, request.baseName, uniqueTag);
|
||||
|
||||
// Destination resolves HERE, after the save gate above, so an unsaved project is
|
||||
// still prompted before any path arithmetic runs. ProjectMedia lands outside the
|
||||
// bank folder and leaves relativePath empty — the Sample it produces indexes
|
||||
// nothing (docs/product/render-in-place.md §"Where the file goes").
|
||||
RenderPaths paths;
|
||||
std::string relativePath;
|
||||
if (request.destination == CaptureDestination::Bank) {
|
||||
const BankPaths bank =
|
||||
deriveBankPaths(projectDir, request.baseName, uniqueTag);
|
||||
paths = RenderPaths{bank.absoluteDir, bank.fileName, bank.fileStem};
|
||||
relativePath = bank.relativePath;
|
||||
} else {
|
||||
std::vector<char> recDir(4096, '\0');
|
||||
GetProjectPathEx(proj, recDir.data(), static_cast<int>(recDir.size()));
|
||||
paths = deriveRenderPaths(std::string(recDir.data()), request.baseName,
|
||||
uniqueTag);
|
||||
if (paths.absoluteDir.empty()) {
|
||||
result.status = CaptureStatus::NoProject;
|
||||
result.message = "Could not resolve the project's recording path.";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
ScopedRenderSettings guard(proj);
|
||||
|
||||
@@ -575,7 +597,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// yield a different value and desync Sample.id from the file name.
|
||||
s.id = "cap-" + uniqueTag + "-" + paths.fileName;
|
||||
s.displayName = request.label();
|
||||
s.relativePath = paths.relativePath; // project-relative (invariant)
|
||||
s.relativePath = relativePath; // project-relative (invariant); empty off the bank
|
||||
s.sourceMode = request.sourceMode;
|
||||
s.sourceRange.startSeconds = request.startSeconds;
|
||||
s.sourceRange.endSeconds = request.endSeconds;
|
||||
@@ -590,12 +612,14 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// single played note, so no root note is derivable; loop points are set
|
||||
// later by an explicit user action.
|
||||
|
||||
result.status = CaptureStatus::Ok;
|
||||
result.sample = s;
|
||||
result.status = CaptureStatus::Ok;
|
||||
result.sample = s;
|
||||
result.absolutePath = expectedPath;
|
||||
result.message = "Captured [" +
|
||||
std::to_string(request.startSeconds) + "s, " +
|
||||
std::to_string(request.endSeconds) + "s] -> " +
|
||||
paths.relativePath + monoCollapseSuffix(collapseOutcome);
|
||||
(relativePath.empty() ? expectedPath : relativePath) +
|
||||
monoCollapseSuffix(collapseOutcome);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ enum class WavBitDepth {
|
||||
Float32,
|
||||
};
|
||||
|
||||
// Where the render lands. TWO VALUES, never a caller-supplied path string: the
|
||||
// backend resolves each to a directory itself, which is what makes "write into the
|
||||
// bank folder" inexpressible from the ProjectMedia side and vice versa.
|
||||
enum class CaptureDestination {
|
||||
Bank, // <projectDir>/reasampler_bank — every capture path
|
||||
ProjectMedia, // the project's recording path — the render-in-place verb only
|
||||
};
|
||||
|
||||
// One capture, independent of source mode.
|
||||
struct CaptureRequest {
|
||||
SourceMode sourceMode = SourceMode::MasterMix;
|
||||
@@ -75,6 +83,9 @@ struct CaptureRequest {
|
||||
// The one home for that fallback rule; both backends populate Sample::displayName
|
||||
// from here rather than each spelling the condition out.
|
||||
std::string label() const { return displayName.empty() ? baseName : displayName; }
|
||||
|
||||
// Default Bank: every existing entry point renders into the bank untouched.
|
||||
CaptureDestination destination = CaptureDestination::Bank;
|
||||
};
|
||||
|
||||
// Every failure is an explicit code, never a thrown exception across the REAPER boundary.
|
||||
@@ -95,6 +106,11 @@ struct CaptureResult {
|
||||
CaptureStatus status = CaptureStatus::RenderFailed;
|
||||
Sample sample; // valid only when status == Ok
|
||||
std::string message; // human-readable detail for the console log
|
||||
|
||||
// The file the render actually landed, absolute — the only handle a caller that
|
||||
// banks nothing has on its own output (sample.relativePath is empty on the
|
||||
// ProjectMedia destination). Set on the Ok path only.
|
||||
std::string absolutePath;
|
||||
};
|
||||
|
||||
// Deterministic offline-render backend: master mix / time selection / selected
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// render_in_place.cpp — see render_in_place.h.
|
||||
//
|
||||
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the
|
||||
// one TU that defines the API pointers; here they are extern.
|
||||
//
|
||||
// Traffic is one-way: this borrows capture's render, and capture may never borrow
|
||||
// this placement back.
|
||||
|
||||
#include "shell/capture/render_in_place.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_name.h" // captureTrackName
|
||||
#include "core/capture/insert_plan.h" // computeInsertMode / InsertOptions
|
||||
#include "core/capture/render_settings.h" // CaptureScope
|
||||
#include "core/capture/tail_control.h" // TailSetting
|
||||
#include "core/capture/track_topology.h" // siblingPlacement
|
||||
#include "core/view/view_mode_model.h" // kArrangeModeId / kDesignModeId
|
||||
#include "shell/capture/capture.h"
|
||||
#include "shell/capture/capture_orchestrator.h" // renderOffline
|
||||
#include "shell/capture/item_read.h" // itemGuid
|
||||
#include "shell/capture/scope_resolve.h" // ResolveScopeSource / trackName
|
||||
#include "shell/capture/track_guid.h" // guidString
|
||||
#include "shell/panel/panel_input.h" // bankPanelTailSetting
|
||||
#include "shell/persist/session.h"
|
||||
#include "shell/view/view.h" // applyMode / mintManagedLanes
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetCursorPosition
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetProjectPathEx
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetTrackColor
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_InsertMedia
|
||||
#define REAPERAPI_WANT_InsertTrackInProject
|
||||
#define REAPERAPI_WANT_SetEditCurPos
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetOnlyTrackSelected
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
namespace {
|
||||
|
||||
void refuse(const std::string& why) {
|
||||
ShowConsoleMsg(("ReaSampler render in place: " + why + "\n").c_str());
|
||||
}
|
||||
|
||||
// Every track's I_FOLDERDEPTH in track order — the flat delta list the pure
|
||||
// sibling arithmetic reads.
|
||||
std::vector<int> folderDepths(ReaProject* proj, int count) {
|
||||
std::vector<int> depths;
|
||||
depths.reserve(static_cast<std::size_t>(count < 0 ? 0 : count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MediaTrack* tr = GetTrack(proj, i);
|
||||
depths.push_back(tr ? static_cast<int>(
|
||||
GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"))
|
||||
: 0);
|
||||
}
|
||||
return depths;
|
||||
}
|
||||
|
||||
int indexOfTrack(ReaProject* proj, int count, MediaTrack* wanted) {
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (GetTrack(proj, i) == wanted) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void setTrackName(MediaTrack* tr, const std::string& name) {
|
||||
// GetSetMediaTrackInfo_String takes a writable buffer even on the set path.
|
||||
std::vector<char> buf(name.begin(), name.end());
|
||||
buf.push_back('\0');
|
||||
GetSetMediaTrackInfo_String(tr, "P_NAME", buf.data(), true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RunRenderTrackInPlace(ReaSamplerSession& session) {
|
||||
ResolvedSource src;
|
||||
std::string why;
|
||||
if (!ResolveScopeSource(CaptureScope::Track, src, why)) { refuse(why); return; }
|
||||
if (src.sourceTracks.empty() || !src.sourceTracks.front()) {
|
||||
refuse("no source track resolved"); return;
|
||||
}
|
||||
|
||||
const TailSetting tail = bankPanelTailSetting();
|
||||
const CaptureName name = captureNameFor(src.trackNames, /*ordinal=*/0, "capture");
|
||||
|
||||
CaptureRequest req;
|
||||
req.sourceMode = SourceMode::SelectedTracks;
|
||||
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = src.endSeconds;
|
||||
req.wetDry = 1.0;
|
||||
req.tailMode = tail.mode;
|
||||
req.tailMs = tail.manualMs;
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.channelCount = 2;
|
||||
req.bitDepth = WavBitDepth::Float32;
|
||||
req.baseName = name.stemBase;
|
||||
req.displayName = name.label;
|
||||
req.destination = CaptureDestination::ProjectMedia;
|
||||
// trackGuids left empty: they exist to stamp provenance onto a Sample this verb
|
||||
// discards. A multi-track selection is refused inside renderOffline, keyed on the
|
||||
// render source, so there is no check to add here.
|
||||
|
||||
const CaptureResult res = renderOffline(CaptureScope::Track, src.sourceTracks, req);
|
||||
if (res.status != CaptureStatus::Ok) { refuse(res.message); return; }
|
||||
|
||||
MediaTrack* source = src.sourceTracks.front();
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
|
||||
const int trackCount = CountTracks(proj);
|
||||
const int srcIndex = indexOfTrack(proj, trackCount, source);
|
||||
if (srcIndex < 0) { refuse("the source track is no longer in the project"); return; }
|
||||
|
||||
const SiblingPlacement place =
|
||||
siblingPlacement(folderDepths(proj, trackCount), srcIndex);
|
||||
|
||||
// Read ONCE, and only to reapply the mode / decide whether the result landed
|
||||
// visible — never to choose a tag. Both tags below are absolute.
|
||||
const std::string activeMode = session.view().activeModeId();
|
||||
|
||||
Undo_BeginBlock2(nullptr);
|
||||
|
||||
// flags = 0, never 1: flags&1 adds default envelopes/FX, and a default chain
|
||||
// would process a render that already carries the source's FX a second time.
|
||||
InsertTrackInProject(proj, place.insertIndex, /*flags=*/0);
|
||||
MediaTrack* fresh = GetTrack(proj, place.insertIndex);
|
||||
if (!fresh) {
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
refuse("could not create the result track");
|
||||
return;
|
||||
}
|
||||
|
||||
// Both writes or none — one alone lands the new track at the wrong nesting level,
|
||||
// which is audible in both directions (see siblingPlacement).
|
||||
if (place.precedingIndex >= 0) {
|
||||
if (MediaTrack* preceding = GetTrack(proj, place.precedingIndex))
|
||||
SetMediaTrackInfo_Value(preceding, "I_FOLDERDEPTH",
|
||||
static_cast<double>(place.precedingDepth));
|
||||
}
|
||||
SetMediaTrackInfo_Value(fresh, "I_FOLDERDEPTH",
|
||||
static_cast<double>(place.newDepth));
|
||||
TrackList_AdjustWindows(false);
|
||||
|
||||
// GetTrackColor returns the colour already OR'd with 0x1000000 and 0 for "no
|
||||
// colour set", which I_CUSTOMCOLOR reads as unused — so one line clones a colour
|
||||
// and the absence of one, with no branch.
|
||||
SetMediaTrackInfo_Value(fresh, "I_CUSTOMCOLOR",
|
||||
static_cast<double>(GetTrackColor(source)));
|
||||
const std::string freshName = captureTrackName(trackName(source));
|
||||
setTrackName(fresh, freshName);
|
||||
|
||||
// Unsnapped and unrounded, deliberately: this placement IS the null test performed
|
||||
// automatically, so snapping it to the grid would move the audio off the position
|
||||
// it was rendered from. InsertOptions{} defaults give native length and no conform.
|
||||
const double cursorPos = GetCursorPosition();
|
||||
SetOnlyTrackSelected(fresh);
|
||||
SetEditCurPos(src.startSeconds, false, false);
|
||||
// InsertMedia's int return isn't SDK-documented; treated conservatively as
|
||||
// 0 = failure, matching performArrangeDrop — an empty result track would
|
||||
// otherwise be a silent no-op, which is exactly what this verb must not produce.
|
||||
const bool placed =
|
||||
InsertMedia(res.absolutePath.c_str(), computeInsertMode(InsertOptions{})) != 0;
|
||||
SetEditCurPos(cursorPos, false, false);
|
||||
// The new track is left selected, alone — in the headline case the source is being
|
||||
// parked out of sight in the same gesture, so restoring the selection would leave
|
||||
// the user selecting an invisible track.
|
||||
|
||||
// Absolute, not mode-following: the source parks on the bench, the result is an
|
||||
// Arrange member whatever mode was active. Explicit records rather than untag(),
|
||||
// because the record is what the panel's auto-tag detector defers to.
|
||||
MembershipIndex& membership = session.view().membership();
|
||||
membership.tag(guidString(source), kDesignModeId);
|
||||
membership.tag(guidString(fresh), kArrangeModeId);
|
||||
|
||||
// The track is brand new, so its items are exactly the ones just placed. An
|
||||
// untagged item would be handed to the detector, which tags to the active mode.
|
||||
const int itemCount = CountTrackMediaItems(fresh);
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
if (MediaItem* it = GetTrackMediaItem(fresh, i)) {
|
||||
const std::string ig = itemGuid(it);
|
||||
if (!ig.empty()) membership.tag(ig, kArrangeModeId);
|
||||
}
|
||||
}
|
||||
|
||||
mintManagedLanes(session.view(), nullptr);
|
||||
applyMode(session.view(), activeMode, nullptr); // a reapply, never a switch
|
||||
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: render selected track to a new track", -1);
|
||||
|
||||
// Persist outside the block. The offline render's own save gate already forced a
|
||||
// saved project, so the Save-As-guarded persist the Design View actions need
|
||||
// cannot have anything to prompt for here.
|
||||
session.saveToActiveProject();
|
||||
|
||||
if (!placed) {
|
||||
refuse("the render landed at " + res.absolutePath +
|
||||
" but REAPER refused to place it — the new track is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Silent on success — the new track is the feedback. Except when it is not: fired
|
||||
// outside Arrange the result track is parked, so a silent success would be
|
||||
// indistinguishable from a no-op.
|
||||
if (activeMode != kArrangeModeId) {
|
||||
ShowConsoleMsg(("ReaSampler render in place: created \"" + freshName +
|
||||
"\" in Arrange (switch to Arrange to see it).\n")
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler::capture
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
// render_in_place — the third verb: render the selected track's output over the
|
||||
// current range to the project's recording path, place it on a new sibling track at
|
||||
// the exact position it was rendered from, and move the source to Design. The bank
|
||||
// is never read, written, or notified (docs/product/render-in-place.md).
|
||||
|
||||
namespace reasampler {
|
||||
class ReaSamplerSession;
|
||||
}
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
// Resolves, renders, creates + dresses the sibling track, places the file, and
|
||||
// settles both tracks' modes in one undo block. Silent on success (the new track is
|
||||
// the feedback) except when the result lands invisible; ShowConsoleMsg on refusal.
|
||||
void RunRenderTrackInPlace(ReaSamplerSession& session);
|
||||
|
||||
} // namespace reasampler::capture
|
||||
@@ -5,6 +5,7 @@
|
||||
// Compiled into the reaper_reasampler MODULE, without REAPERAPI_IMPLEMENT (main.cpp
|
||||
// owns the API pointers). DAW-verified, not unit-tested.
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -163,11 +164,19 @@ bool detectNewContent() {
|
||||
std::map<std::string, std::vector<std::string>> trackItemGuids;
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane, trackItemGuids);
|
||||
|
||||
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
if (added.empty()) return false; // first poll after open, or nothing new this tick
|
||||
|
||||
ViewModeModel& model = g_panel.session->view();
|
||||
|
||||
// An explicit tag wins: this detector classifies content the USER made, not
|
||||
// content the tool made and already classified.
|
||||
added.erase(std::remove_if(added.begin(), added.end(),
|
||||
[&](const std::string& g) {
|
||||
return model.membership().query(g) != nullptr;
|
||||
}),
|
||||
added.end());
|
||||
|
||||
// Which of `added` are items (the manual-lane map keys every item; track GUIDs never
|
||||
// appear there). Used below to exclude sibling new items from a track's PRE-EXISTING
|
||||
// mode set — a drop plus its own new siblings must not count each other as prior.
|
||||
|
||||
Reference in New Issue
Block a user