script revisions FINAL
- automatically determines Renders, Submodules, and Daily folder
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -23,43 +23,72 @@ const JOB_TYPE = {
|
|||||||
description: "Number of frames to render in one Blender render task",
|
description: "Number of frames to render in one Blender render task",
|
||||||
visible: "submission"
|
visible: "submission"
|
||||||
},
|
},
|
||||||
|
// Ensure blendfile is available for subsequent auto-evals
|
||||||
|
{
|
||||||
|
key: "blendfile",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
eval: "bpy.data.filepath",
|
||||||
|
description: "Path of the Blend file to render",
|
||||||
|
visible: "web"
|
||||||
|
},
|
||||||
|
|
||||||
// render_output_root + add_path_components determine the value of render_output_path.
|
|
||||||
{
|
{
|
||||||
key: "render_output_root",
|
key: "render_output_root",
|
||||||
type: "string",
|
type: "string",
|
||||||
subtype: "dir_path",
|
subtype: "dir_path",
|
||||||
required: true,
|
required: false,
|
||||||
default: '//',
|
visible: "submission",
|
||||||
visible: "submission",
|
eval: "__import__('os').path.normpath(__import__('os').path.join(((__import__('re').search(r'^(.*?)[\\/][Bb]lends[\\/]', bpy.data.filepath.replace('\\\\','/')) and __import__('re').search(r'^(.*?)[\\/][Bb]lends[\\/]', bpy.data.filepath.replace('\\\\','/')).group(1)) or __import__('os').path.dirname(bpy.data.filepath)), 'Renders'))",
|
||||||
description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"
|
evalInfo: {
|
||||||
},
|
showLinkButton: true,
|
||||||
{
|
description: "Auto-detect the project's Renders folder"
|
||||||
key: "add_path_components",
|
},
|
||||||
type: "int32",
|
description: "Base path where renders are stored, typically the project's Renders folder. If empty, derived automatically."
|
||||||
required: true,
|
},
|
||||||
default: 0,
|
{
|
||||||
propargs: {min: 0, max: 32},
|
key: "daily_path",
|
||||||
visible: "submission",
|
type: "string",
|
||||||
description: "Number of path components of the current blend file to use in the render output path"
|
required: false,
|
||||||
|
visible: "submission",
|
||||||
|
eval: "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
|
||||||
|
evalInfo: {
|
||||||
|
showLinkButton: true,
|
||||||
|
description: "Auto-fill with today's daily folder name"
|
||||||
|
},
|
||||||
|
description: "Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "use_submodule",
|
||||||
|
label: "Use Submodule",
|
||||||
|
type: "bool",
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
visible: "submission",
|
||||||
|
description: "Include a submodule folder under Renders. Turn off to omit submodule entirely."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "submodule",
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
visible: "submission",
|
||||||
|
eval: "(__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath)) if settings.use_submodule else '')",
|
||||||
|
evalInfo: {
|
||||||
|
showLinkButton: true,
|
||||||
|
description: "Auto-fill with the current .blend file's parent folder"
|
||||||
|
},
|
||||||
|
description: "Optional submodule under Renders (e.g. 'Waterspider B'). If empty, omitted."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "render_output_path",
|
key: "render_output_path",
|
||||||
type: "string",
|
type: "string",
|
||||||
subtype: "file_path",
|
subtype: "file_path",
|
||||||
editable: false,
|
editable: false,
|
||||||
eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, jobname + '_######'))",
|
eval: "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
|
||||||
description: "Final file path of where render output will be saved"
|
description: "Final file path of where render output will be saved"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Automatically evaluated settings:
|
// Automatically evaluated settings:
|
||||||
{
|
|
||||||
key: "blendfile",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
description: "Path of the Blend file to render",
|
|
||||||
visible: "web"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "fps",
|
key: "fps",
|
||||||
type: "float",
|
type: "float",
|
||||||
@@ -119,11 +148,37 @@ function compileJob(job) {
|
|||||||
print("job: ", job);
|
print("job: ", job);
|
||||||
|
|
||||||
const settings = job.settings;
|
const settings = job.settings;
|
||||||
|
|
||||||
|
// Ensure auto-filled values are applied at submission time.
|
||||||
|
try {
|
||||||
|
if (settings.use_submodule) {
|
||||||
|
const detectedSubmodule = detectSubmodule(settings) || '';
|
||||||
|
if (!settings.submodule || String(settings.submodule).trim() === '') {
|
||||||
|
settings.submodule = detectedSubmodule;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settings.submodule = '';
|
||||||
|
}
|
||||||
|
if (!settings.render_output_root || String(settings.render_output_root).trim() === '') {
|
||||||
|
// Auto-detect project root and Renders folder similar to eval button
|
||||||
|
const projectRoot = findProjectRootFromBlendfile(settings.blendfile);
|
||||||
|
if (projectRoot) settings.render_output_root = path.join(projectRoot, 'Renders');
|
||||||
|
else settings.render_output_root = path.join(path.dirname(settings.blendfile), 'Renders');
|
||||||
|
}
|
||||||
|
if (!settings.daily_path || String(settings.daily_path).trim() === '') {
|
||||||
|
const createdDate = job && job.created ? new Date(job.created) : new Date();
|
||||||
|
settings.daily_path = formatDailyYYMMDD(createdDate);
|
||||||
|
}
|
||||||
|
const recomposed = computeAutoRenderOutputPath(job);
|
||||||
|
if (recomposed) settings.render_output_path = recomposed;
|
||||||
|
} catch (e) {
|
||||||
|
print("Auto-fill on submit failed:", e);
|
||||||
|
}
|
||||||
if (videoFormats.indexOf(settings.format) >= 0) {
|
if (videoFormats.indexOf(settings.format) >= 0) {
|
||||||
throw `This job type only renders images, and not "${settings.format}"`;
|
throw `This job type only renders images, and not "${settings.format}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderOutput = renderOutputPath(job);
|
const renderOutput = normalizePathSeparators(settings.render_output_path || renderOutputPath(job));
|
||||||
|
|
||||||
// Make sure that when the job is investigated later, it shows the
|
// Make sure that when the job is investigated later, it shows the
|
||||||
// actually-used render output:
|
// actually-used render output:
|
||||||
@@ -145,6 +200,110 @@ function compileJob(job) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive project root, submodule, and daily path from the blendfile path.
|
||||||
|
function computeAutoRenderOutputPath(job) {
|
||||||
|
const settings = job.settings || {};
|
||||||
|
if (!settings.blendfile) return null;
|
||||||
|
|
||||||
|
const projectRoot = findProjectRootFromBlendfile(settings.blendfile);
|
||||||
|
|
||||||
|
const submodule = (settings.submodule && ("" + settings.submodule).trim()) ? ("" + settings.submodule).trim() : detectSubmodule(settings);
|
||||||
|
|
||||||
|
// Resolve render root
|
||||||
|
let renderRoot = null;
|
||||||
|
if (settings.render_output_root && ("" + settings.render_output_root).trim()) {
|
||||||
|
renderRoot = ("" + settings.render_output_root).trim();
|
||||||
|
} else if (projectRoot) {
|
||||||
|
renderRoot = path.join(projectRoot, 'Renders');
|
||||||
|
} else {
|
||||||
|
// Fallback to the blendfile's directory Renders sibling
|
||||||
|
renderRoot = path.join(path.dirname(settings.blendfile), 'Renders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve daily path
|
||||||
|
let daily = null;
|
||||||
|
if (settings.daily_path && ("" + settings.daily_path).trim()) {
|
||||||
|
daily = ("" + settings.daily_path).trim();
|
||||||
|
} else {
|
||||||
|
const createdDate = job && job.created ? new Date(job.created) : new Date();
|
||||||
|
daily = formatDailyYYMMDD(createdDate);
|
||||||
|
}
|
||||||
|
const jobname = (job && job.name) ? job.name : path.stem(settings.blendfile).replace('.flamenco', '');
|
||||||
|
|
||||||
|
print('AutoPath: blendfile=', settings.blendfile);
|
||||||
|
print('AutoPath: projectRoot=', projectRoot);
|
||||||
|
print('AutoPath: renderRoot=', renderRoot);
|
||||||
|
print('AutoPath: submodule=', submodule);
|
||||||
|
print('AutoPath: daily=', daily);
|
||||||
|
print('AutoPath: jobname=', jobname);
|
||||||
|
|
||||||
|
const parts = [renderRoot];
|
||||||
|
if (submodule) parts.push(submodule);
|
||||||
|
parts.push(daily, jobname, `${jobname}_######`);
|
||||||
|
const finalPath = path.join.apply(path, parts);
|
||||||
|
print('AutoPath: finalPath=', finalPath);
|
||||||
|
return finalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProjectRootFromBlendfile(blendfilePath) {
|
||||||
|
const blendDir = path.dirname(blendfilePath);
|
||||||
|
const normalized = blendDir.replace(/\\/g, '/');
|
||||||
|
const parts = normalized.split('/');
|
||||||
|
|
||||||
|
let blendsIndex = -1;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].toLowerCase() === 'blends') {
|
||||||
|
blendsIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (blendsIndex <= 0) return null;
|
||||||
|
const rootParts = parts.slice(0, blendsIndex);
|
||||||
|
if (rootParts.length === 0) return null;
|
||||||
|
return rootParts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSubmoduleFromBlendfile(blendfilePath) {
|
||||||
|
const blendDir = path.dirname(blendfilePath);
|
||||||
|
const normalized = blendDir.replace(/\\/g, '/');
|
||||||
|
const parts = normalized.split('/');
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].toLowerCase() === 'blends') {
|
||||||
|
if (i + 1 < parts.length && parts[i + 1].toLowerCase() === 'animations') {
|
||||||
|
if (i + 2 < parts.length) return parts[i + 2];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer explicit setting; else detect robustly from blendfile path.
|
||||||
|
function detectSubmodule(settings) {
|
||||||
|
if (!settings) return null;
|
||||||
|
if (settings.submodule && ("" + settings.submodule).trim()) {
|
||||||
|
return ("" + settings.submodule).trim();
|
||||||
|
}
|
||||||
|
const bf = settings.blendfile || '';
|
||||||
|
// Try regex first (case-insensitive): /Blends/animations/<name>/...
|
||||||
|
try {
|
||||||
|
const bfNorm = bf.replace(/\\/g, '/');
|
||||||
|
const m = bfNorm.match(/\/(?:[Bb]lends)\/(?:[Aa]nimations)\/([^\/]+)/);
|
||||||
|
print('detectSubmodule: bf=', bfNorm, ' match=', m && m[1]);
|
||||||
|
if (m && m[1]) return m[1];
|
||||||
|
} catch (_) {}
|
||||||
|
return detectSubmoduleFromBlendfile(bf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDailyYYMMDD(dateObj) {
|
||||||
|
const pad2 = (n) => (n < 10 ? '0' + n : '' + n);
|
||||||
|
const yy = (dateObj.getFullYear() % 100);
|
||||||
|
const mm = dateObj.getMonth() + 1;
|
||||||
|
const dd = dateObj.getDate();
|
||||||
|
return `daily_${pad2(yy)}${pad2(mm)}${pad2(dd)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Do field replacement on the render output path.
|
// Do field replacement on the render output path.
|
||||||
function renderOutputPath(job) {
|
function renderOutputPath(job) {
|
||||||
let path = job.settings.render_output_path;
|
let path = job.settings.render_output_path;
|
||||||
@@ -161,6 +320,14 @@ function renderOutputPath(job) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure consistent separators for server-side consumption.
|
||||||
|
function normalizePathSeparators(p) {
|
||||||
|
if (!p) return p;
|
||||||
|
const forward = p.replace(/\\/g, '/');
|
||||||
|
// Collapse multiple slashes but preserve drive letter paths like 'A:/'
|
||||||
|
return forward.replace(/([^:])\/+/g, '$1/');
|
||||||
|
}
|
||||||
|
|
||||||
function authorRenderTasks(settings, renderDir, renderOutput) {
|
function authorRenderTasks(settings, renderDir, renderOutput) {
|
||||||
print("authorRenderTasks(", renderDir, renderOutput, ")");
|
print("authorRenderTasks(", renderDir, renderOutput, ")");
|
||||||
let renderTasks = [];
|
let renderTasks = [];
|
||||||
|
|||||||
@@ -25,33 +25,53 @@ const JOB_TYPE = {
|
|||||||
visible: 'submission',
|
visible: 'submission',
|
||||||
},
|
},
|
||||||
|
|
||||||
// render_output_root + add_path_components determine the value of render_output_path.
|
|
||||||
{
|
{
|
||||||
key: 'render_output_root',
|
key: 'render_output_root',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
subtype: 'dir_path',
|
subtype: 'dir_path',
|
||||||
required: true,
|
required: false,
|
||||||
default: '//',
|
|
||||||
visible: 'submission',
|
visible: 'submission',
|
||||||
|
eval:
|
||||||
|
"__import__('os').path.normpath(__import__('os').path.join(((__import__('re').search(r'^(.*?)[\\/][Bb]lends[\\/]', bpy.data.filepath.replace('\\\\','/')) and __import__('re').search(r'^(.*?)[\\/][Bb]lends[\\/]', bpy.data.filepath.replace('\\\\','/')).group(1)) or __import__('os').path.dirname(bpy.data.filepath)), 'Renders'))",
|
||||||
|
evalInfo: { showLinkButton: true, description: "Auto-detect the project's Renders folder" },
|
||||||
description:
|
description:
|
||||||
'Base directory of where render output is stored. Will have some job-specific parts appended to it',
|
"Base path where renders are stored, typically the project's Renders folder. If empty, derived automatically.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'add_path_components',
|
key: 'daily_path',
|
||||||
type: 'int32',
|
type: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
default: 0,
|
|
||||||
propargs: { min: 0, max: 32 },
|
|
||||||
visible: 'submission',
|
visible: 'submission',
|
||||||
|
eval: "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
|
||||||
|
evalInfo: { showLinkButton: true, description: "Auto-fill with today's daily folder name" },
|
||||||
description:
|
description:
|
||||||
'Number of path components of the current blend file to use in the render output path',
|
"Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'use_submodule',
|
||||||
|
label: 'Use Submodule',
|
||||||
|
type: 'bool',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
visible: 'submission',
|
||||||
|
description: 'Include a submodule folder under Renders. Turn off to omit submodule entirely.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'submodule',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
visible: 'submission',
|
||||||
|
eval: "(__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath)) if settings.use_submodule else '')",
|
||||||
|
evalInfo: { showLinkButton: true, description: "Auto-fill with the current .blend file's parent folder" },
|
||||||
|
description:
|
||||||
|
"Optional submodule under Renders (e.g. 'Waterspider B'). If empty, omitted.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'render_output_path',
|
key: 'render_output_path',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
subtype: 'file_path',
|
subtype: 'file_path',
|
||||||
editable: false,
|
editable: false,
|
||||||
eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, jobname + '_######'))",
|
eval: "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
|
||||||
description: 'Final file path of where render output will be saved',
|
description: 'Final file path of where render output will be saved',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,6 +111,7 @@ const JOB_TYPE = {
|
|||||||
key: 'blendfile',
|
key: 'blendfile',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
|
eval: 'bpy.data.filepath',
|
||||||
description: 'Path of the Blend file to render',
|
description: 'Path of the Blend file to render',
|
||||||
visible: 'web',
|
visible: 'web',
|
||||||
},
|
},
|
||||||
@@ -144,11 +165,35 @@ function compileJob(job) {
|
|||||||
print('job: ', job);
|
print('job: ', job);
|
||||||
|
|
||||||
const settings = job.settings;
|
const settings = job.settings;
|
||||||
|
// Ensure auto-filled values are applied at submission time.
|
||||||
|
try {
|
||||||
|
if (settings.use_submodule) {
|
||||||
|
const detectedSubmodule = detectSubmodule(settings) || '';
|
||||||
|
if (!settings.submodule || String(settings.submodule).trim() === '') {
|
||||||
|
settings.submodule = detectedSubmodule;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settings.submodule = '';
|
||||||
|
}
|
||||||
|
if (!settings.render_output_root || String(settings.render_output_root).trim() === '') {
|
||||||
|
const projectRoot = findProjectRootFromBlendfile(settings.blendfile);
|
||||||
|
if (projectRoot) settings.render_output_root = path.join(projectRoot, 'Renders');
|
||||||
|
else settings.render_output_root = path.join(path.dirname(settings.blendfile), 'Renders');
|
||||||
|
}
|
||||||
|
if (!settings.daily_path || String(settings.daily_path).trim() === '') {
|
||||||
|
const createdDate = job && job.created ? new Date(job.created) : new Date();
|
||||||
|
settings.daily_path = formatDailyYYMMDD(createdDate);
|
||||||
|
}
|
||||||
|
const recomposed = computeAutoRenderOutputPath(job);
|
||||||
|
if (recomposed) settings.render_output_path = recomposed;
|
||||||
|
} catch (e) {
|
||||||
|
print('Auto-fill on submit failed:', e);
|
||||||
|
}
|
||||||
if (videoFormats.indexOf(settings.format) >= 0) {
|
if (videoFormats.indexOf(settings.format) >= 0) {
|
||||||
throw `This job type only renders images, and not "${settings.format}"`;
|
throw `This job type only renders images, and not "${settings.format}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderOutput = renderOutputPath(job);
|
const renderOutput = normalizePathSeparators(settings.render_output_path || renderOutputPath(job));
|
||||||
|
|
||||||
// Make sure that when the job is investigated later, it shows the
|
// Make sure that when the job is investigated later, it shows the
|
||||||
// actually-used render output:
|
// actually-used render output:
|
||||||
@@ -188,6 +233,14 @@ function renderOutputPath(job) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure consistent separators for server-side consumption.
|
||||||
|
function normalizePathSeparators(p) {
|
||||||
|
if (!p) return p;
|
||||||
|
const forward = p.replace(/\\/g, '/');
|
||||||
|
// Collapse multiple slashes but preserve drive letter paths like 'A:/'
|
||||||
|
return forward.replace(/([^:])\/+/g, '$1/');
|
||||||
|
}
|
||||||
|
|
||||||
const enable_all_optix = `
|
const enable_all_optix = `
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
@@ -320,3 +373,92 @@ function cleanupJobSettings(settings) {
|
|||||||
if (!settings[setting_name]) delete settings[setting_name];
|
if (!settings[setting_name]) delete settings[setting_name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive project root, submodule, and daily path from the blendfile path.
|
||||||
|
function computeAutoRenderOutputPath(job) {
|
||||||
|
const settings = job.settings || {};
|
||||||
|
if (!settings.blendfile) return null;
|
||||||
|
|
||||||
|
const projectRoot = findProjectRootFromBlendfile(settings.blendfile);
|
||||||
|
|
||||||
|
const submodule = detectSubmodule(settings);
|
||||||
|
// Resolve render root
|
||||||
|
let renderRoot = null;
|
||||||
|
if (settings.render_output_root && ("" + settings.render_output_root).trim()) {
|
||||||
|
renderRoot = ("" + settings.render_output_root).trim();
|
||||||
|
} else if (projectRoot) {
|
||||||
|
renderRoot = path.join(projectRoot, 'Renders');
|
||||||
|
} else {
|
||||||
|
renderRoot = path.join(path.dirname(settings.blendfile), 'Renders');
|
||||||
|
}
|
||||||
|
// Resolve daily path
|
||||||
|
let daily = null;
|
||||||
|
if (settings.daily_path && ("" + settings.daily_path).trim()) {
|
||||||
|
daily = ("" + settings.daily_path).trim();
|
||||||
|
} else {
|
||||||
|
const createdDate = job && job.created ? new Date(job.created) : new Date();
|
||||||
|
daily = formatDailyYYMMDD(createdDate);
|
||||||
|
}
|
||||||
|
const jobname = job && job.name ? job.name : path.stem(settings.blendfile).replace('.flamenco', '');
|
||||||
|
|
||||||
|
const parts = [renderRoot];
|
||||||
|
if (submodule) parts.push(submodule);
|
||||||
|
parts.push(daily, jobname, `${jobname}_######`);
|
||||||
|
return path.join.apply(path, parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProjectRootFromBlendfile(blendfilePath) {
|
||||||
|
const blendDir = path.dirname(blendfilePath);
|
||||||
|
const normalized = blendDir.replace(/\\/g, '/');
|
||||||
|
const parts = normalized.split('/');
|
||||||
|
|
||||||
|
let blendsIndex = -1;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].toLowerCase() === 'blends') {
|
||||||
|
blendsIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (blendsIndex <= 0) return null;
|
||||||
|
const rootParts = parts.slice(0, blendsIndex);
|
||||||
|
if (rootParts.length === 0) return null;
|
||||||
|
return rootParts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectSubmoduleFromBlendfile(blendfilePath) {
|
||||||
|
const blendDir = path.dirname(blendfilePath);
|
||||||
|
const normalized = blendDir.replace(/\\/g, '/');
|
||||||
|
const parts = normalized.split('/');
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].toLowerCase() === 'blends') {
|
||||||
|
if (i + 1 < parts.length && parts[i + 1].toLowerCase() === 'animations') {
|
||||||
|
if (i + 2 < parts.length) return parts[i + 2];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer explicit setting; else detect robustly from blendfile path.
|
||||||
|
function detectSubmodule(settings) {
|
||||||
|
if (!settings) return null;
|
||||||
|
if (settings.submodule && ("" + settings.submodule).trim()) {
|
||||||
|
return ("" + settings.submodule).trim();
|
||||||
|
}
|
||||||
|
const bf = settings.blendfile || '';
|
||||||
|
try {
|
||||||
|
const m = bf.replace(/\\/g, '/').match(/\/(?:[Bb]lends)\/(?:[Aa]nimations)\/([^\/]+)/);
|
||||||
|
if (m && m[1]) return m[1];
|
||||||
|
} catch (_) {}
|
||||||
|
return detectSubmoduleFromBlendfile(bf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDailyYYMMDD(dateObj) {
|
||||||
|
const pad2 = (n) => (n < 10 ? '0' + n : '' + n);
|
||||||
|
const yy = dateObj.getFullYear() % 100;
|
||||||
|
const mm = dateObj.getMonth() + 1;
|
||||||
|
const dd = dateObj.getDate();
|
||||||
|
return `daily_${pad2(yy)}${pad2(mm)}${pad2(dd)}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user