@bitmagic/cli 0.1.64-dev.3 → 0.1.64-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -3
- package/dist/assets/keep-glb.d.ts +20 -0
- package/dist/assets/keep-glb.js +45 -0
- package/dist/assets/keep-glb.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.d.ts +3 -0
- package/dist/commands/assets.js +33 -3
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/cover.js +7 -1
- package/dist/commands/cover.js.map +1 -1
- package/dist/commands/generate.d.ts +54 -1
- package/dist/commands/generate.js +294 -20
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +33 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/judge.d.ts +0 -11
- package/dist/commands/judge.js +15 -2
- package/dist/commands/judge.js.map +1 -1
- package/dist/commands/reference.js +5 -1
- package/dist/commands/reference.js.map +1 -1
- package/dist/commands/tools.d.ts +1 -0
- package/dist/commands/tools.js +73 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/generate/animation-from-video.d.ts +11 -3
- package/dist/generate/animation-from-video.js +12 -2
- package/dist/generate/animation-from-video.js.map +1 -1
- package/dist/generate/model.d.ts +11 -0
- package/dist/generate/model.js +41 -1
- package/dist/generate/model.js.map +1 -1
- package/dist/generate/prop.d.ts +9 -0
- package/dist/generate/prop.js +59 -9
- package/dist/generate/prop.js.map +1 -1
- package/dist/generate/video-review.d.ts +28 -0
- package/dist/generate/video-review.js +120 -0
- package/dist/generate/video-review.js.map +1 -0
- package/dist/project/art-style.d.ts +34 -0
- package/dist/project/art-style.js +63 -0
- package/dist/project/art-style.js.map +1 -0
- package/dist/scaffold/blender-level-skill-files.js +25 -3
- package/dist/scaffold/blender-level-skill-files.js.map +1 -1
- package/dist/scaffold/claude-settings.js +2 -0
- package/dist/scaffold/claude-settings.js.map +1 -1
- package/dist/scaffold/project-files.d.ts +2 -1
- package/dist/scaffold/project-files.js +46 -35
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.d.ts +9 -0
- package/dist/scaffold/project.js +23 -3
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/reference-guidance.d.ts +1 -1
- package/dist/scaffold/reference-guidance.js +2 -1
- package/dist/scaffold/reference-guidance.js.map +1 -1
- package/dist/scaffold/skill-files.js +1 -1
- package/dist/scaffold/unity-skill-files.js +1 -2
- package/dist/scaffold/unity-skill-files.js.map +1 -1
- package/dist/tools/bundles.d.ts +21 -0
- package/dist/tools/bundles.js +67 -0
- package/dist/tools/bundles.js.map +1 -0
- package/dist/trailer/ffmpeg.d.ts +10 -0
- package/dist/trailer/ffmpeg.js +51 -6
- package/dist/trailer/ffmpeg.js.map +1 -1
- package/dist/trailer/music-beats.js +4 -39
- package/dist/trailer/music-beats.js.map +1 -1
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import { type GenerateModelResult } from '../generate/model.js';
|
|
|
6
6
|
import { type AnimationFromVideoRun } from '../generate/animation-from-video.js';
|
|
7
7
|
import { type VideoRun } from '../generate/video.js';
|
|
8
8
|
import type { VoxelizeGlb } from '../assets/voxelize-glb.js';
|
|
9
|
+
import type { KeepGlb } from '../assets/keep-glb.js';
|
|
9
10
|
export interface SkyboxArgs {
|
|
10
11
|
description: string;
|
|
11
12
|
referenceImageUrl?: string;
|
|
@@ -61,6 +62,8 @@ export interface CharacterArgs {
|
|
|
61
62
|
headScale?: string;
|
|
62
63
|
/** citty's negative boolean: `--no-library` arrives as `false`; the flag has no positive form. */
|
|
63
64
|
library?: boolean;
|
|
65
|
+
/** Absent = the generator's default (true); the command fills it from the project's art style. */
|
|
66
|
+
voxelize?: boolean;
|
|
64
67
|
}
|
|
65
68
|
export declare function buildCharacterBody(args: CharacterArgs): Record<string, unknown>;
|
|
66
69
|
export interface AnimationArgs {
|
|
@@ -102,7 +105,32 @@ export interface AnimationVideoArgs {
|
|
|
102
105
|
/** Any of the authored-spec flags, to refuse the two forms being mixed. */
|
|
103
106
|
spec?: string;
|
|
104
107
|
family?: string;
|
|
108
|
+
/** The prompt lane, to refuse a clip and a description being given at once. */
|
|
109
|
+
videoPrompt?: string;
|
|
105
110
|
}
|
|
111
|
+
export interface AnimationPromptArgs extends Omit<AnimationVideoArgs, 'video'> {
|
|
112
|
+
videoPrompt: string;
|
|
113
|
+
videoDuration?: number;
|
|
114
|
+
video?: string;
|
|
115
|
+
}
|
|
116
|
+
/** A `--video-prompt` run: everything `--video` carries, plus the clip to generate first. */
|
|
117
|
+
export interface AnimationFromPromptRun {
|
|
118
|
+
/** What the source clip should show, in the creator's words. */
|
|
119
|
+
description: string;
|
|
120
|
+
/** Source clip length in seconds; billed per second by the video leg. */
|
|
121
|
+
durationSeconds: number;
|
|
122
|
+
run: AnimationFromVideoRun;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `--video-prompt` is `--video` with the clip generated instead of filmed, so it
|
|
126
|
+
* shares every downstream flag and refuses the same combinations. The extra
|
|
127
|
+
* refusal is the duration floor: `generate video` will happily make a 1 s clip,
|
|
128
|
+
* and Uthana will then reject it, which would bill the creator for footage that
|
|
129
|
+
* never had a chance.
|
|
130
|
+
*/
|
|
131
|
+
export declare function buildAnimationFromPromptRun(args: AnimationPromptArgs): AnimationFromPromptRun;
|
|
132
|
+
/** `--video` takes either a local file or an already-public clip URL. */
|
|
133
|
+
export declare function isHttpUrl(value: string): boolean;
|
|
106
134
|
/**
|
|
107
135
|
* `--video` is a different generator from the authored spec: the clip comes from a person, not
|
|
108
136
|
* a parameter file, so the two flag sets cannot be combined and the spec flags are refused here
|
|
@@ -112,6 +140,8 @@ export declare function buildAnimationVideoRun(args: AnimationVideoArgs): Animat
|
|
|
112
140
|
export interface GenerateRunDeps extends CommandRunDeps {
|
|
113
141
|
/** Test-only override for the headless bake, so the lane runs without Chrome. */
|
|
114
142
|
voxelize?: VoxelizeGlb;
|
|
143
|
+
/** Injected in tests so the kept-mesh lane (`--keep-glb` / low-poly projects) runs without a network. */
|
|
144
|
+
keepGlbUpload?: KeepGlb;
|
|
115
145
|
}
|
|
116
146
|
/**
|
|
117
147
|
* Runs one `bitmagic generate <subcommand>`: requests the asset, then applies the returned
|
|
@@ -222,6 +252,9 @@ export declare function runPropGeneration(options: {
|
|
|
222
252
|
voxelGrid?: number;
|
|
223
253
|
/** `--no-materials`: leave the asset matte rather than classifying it after the bake. */
|
|
224
254
|
materials?: boolean;
|
|
255
|
+
/** `--keep-glb` / `--voxelize`; absent follows game.json artStyle. */
|
|
256
|
+
keepGlb?: boolean;
|
|
257
|
+
voxelize?: boolean;
|
|
225
258
|
json: boolean;
|
|
226
259
|
deps?: GenerateRunDeps;
|
|
227
260
|
}): Promise<PropVoxelizeResult>;
|
|
@@ -271,6 +304,9 @@ export declare function runModelGeneration(options: {
|
|
|
271
304
|
voxelGrid?: number;
|
|
272
305
|
/** `--no-materials`: leave the asset matte rather than classifying it after the bake. */
|
|
273
306
|
materials?: boolean;
|
|
307
|
+
/** `--keep-glb` / `--voxelize`; absent follows game.json artStyle. */
|
|
308
|
+
keepGlb?: boolean;
|
|
309
|
+
voxelize?: boolean;
|
|
274
310
|
json: boolean;
|
|
275
311
|
deps?: GenerateRunDeps;
|
|
276
312
|
}): Promise<GenerateModelResult>;
|
|
@@ -280,9 +316,26 @@ export declare function runModelGeneration(options: {
|
|
|
280
316
|
* job record, so the dev view shows it beside every other generation.
|
|
281
317
|
*/
|
|
282
318
|
export declare function runAnimationFromVideo(run: AnimationFromVideoRun, json: boolean, deps?: GenerateRunDeps): Promise<void>;
|
|
319
|
+
/**
|
|
320
|
+
* The `--video-prompt` lane: text -> clip -> motion, the two existing job lanes run back to back.
|
|
321
|
+
*
|
|
322
|
+
* There is no chained endpoint behind this — asset-forger has one route that makes a video and
|
|
323
|
+
* another that reads one, and this is the seam between them. Both legs bill under their own
|
|
324
|
+
* existing key (`generate-video` per second, `generate-animation-from-video` flat), the way
|
|
325
|
+
* design-vehicle prices its chained image sub-generations rather than minting a composite.
|
|
326
|
+
*
|
|
327
|
+
* Both asset rows are kept. The clip is a real `type: 'video'` asset the creator can play back
|
|
328
|
+
* or reuse as a cutscene, and keeping it is also what makes a leg-2 failure recoverable: the
|
|
329
|
+
* footage is already paid for, so the error hands back a `--video <url>` retry instead of asking
|
|
330
|
+
* for the whole thing again.
|
|
331
|
+
*/
|
|
332
|
+
export declare function runAnimationFromPrompt(prompt: AnimationFromPromptRun, json: boolean, deps?: GenerateRunDeps, creatorPrompt?: string): Promise<void>;
|
|
283
333
|
/**
|
|
284
334
|
* Job-shaped rather than streamed (see generate/video.ts), so it cannot share `runAssetGeneration`;
|
|
285
335
|
* it does share the job record, so the dev view shows it beside every other generation.
|
|
286
336
|
*/
|
|
287
|
-
export declare function runVideoGeneration(run: VideoRun, json: boolean,
|
|
337
|
+
export declare function runVideoGeneration(run: VideoRun, json: boolean, options?: {
|
|
338
|
+
deps?: GenerateRunDeps;
|
|
339
|
+
contactSheet?: boolean;
|
|
340
|
+
}): Promise<void>;
|
|
288
341
|
export declare const generateCommand: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
@@ -9,7 +9,8 @@ import { CliError, errnoCode } from '../errors.js';
|
|
|
9
9
|
import { parseNumberFlag } from './number-flag.js';
|
|
10
10
|
import { createOutput } from '../output.js';
|
|
11
11
|
import { commandRunDeps, decoratedError } from './run-deps.js';
|
|
12
|
-
import { loadProjectContext } from '../project/context.js';
|
|
12
|
+
import { findProjectRootOrNull, loadProjectContext } from '../project/context.js';
|
|
13
|
+
import { readArtStyleDeclaration, resolveArtStyle } from '../project/art-style.js';
|
|
13
14
|
import { withJobRecord } from '../project/jobs.js';
|
|
14
15
|
import { describeAppliedReference, NO_REFERENCE_FLAG, REFERENCE_FILE_FLAG, REFERENCE_IMAGE_FLAG, } from '../project/reference.js';
|
|
15
16
|
import { resolveReferenceForCall } from '../project/reference-file.js';
|
|
@@ -26,8 +27,9 @@ import { downloadMusicFile } from '../generate/music.js';
|
|
|
26
27
|
import { MUSIC_MAX_SECONDS, MUSIC_MIN_SECONDS, VIDEO_MAX_SECONDS, VIDEO_MIN_SECONDS } from '@bitmagic/asset-core';
|
|
27
28
|
import { generateAndInstallAnimationFromVideo, isLocomotionState, isVideoToMotionModel, } from '../generate/animation-from-video.js';
|
|
28
29
|
import { generateAndInstallVideo, IMAGE_EXTENSIONS, } from '../generate/video.js';
|
|
30
|
+
import { reviewGeneratedClip } from '../generate/video-review.js';
|
|
29
31
|
import { VIDEO_EXTENSIONS } from '../assets/file-kind.js';
|
|
30
|
-
import { LOCOMOTION_STATES, VIDEO_TO_MOTION_MODELS } from '@bitmagic/asset-core';
|
|
32
|
+
import { LOCOMOTION_STATES, VIDEO_TO_MOTION_MODELS, MOTION_SOURCE_DEFAULT_SECONDS, MOTION_SOURCE_MIN_SECONDS, buildMotionSourcePrompt, } from '@bitmagic/asset-core';
|
|
31
33
|
import { classifyAssetMaterialsQuietly } from '../assets/materials.js';
|
|
32
34
|
import { DEFAULT_MAX_VOXEL_SIZE, DEFAULT_MIN_VOXEL_SIZE, SOLID_FILL_MAX_HEIGHT_M } from './assets.js';
|
|
33
35
|
import { creatorPromptField, ORIGINAL_PROMPT_FLAG, withParent } from '../telemetry/creator-prompt.js';
|
|
@@ -102,6 +104,18 @@ export function buildImageBody(args) {
|
|
|
102
104
|
referenceImageUrl: args.referenceImageUrl,
|
|
103
105
|
});
|
|
104
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The character default a project's art style implies: a low-poly game keeps the textured rig.
|
|
109
|
+
* Read from the cwd's project like every other generate command; outside a project the answer
|
|
110
|
+
* is "declares nothing", so the generator's own default (voxelize) stands and `loadProjectContext`
|
|
111
|
+
* reports the missing project on the very next line.
|
|
112
|
+
*/
|
|
113
|
+
function defaultVoxelizeForProject() {
|
|
114
|
+
const root = findProjectRootOrNull();
|
|
115
|
+
if (root === null)
|
|
116
|
+
return undefined;
|
|
117
|
+
return readArtStyleDeclaration(root).artStyle === 'low-poly' ? false : undefined;
|
|
118
|
+
}
|
|
105
119
|
export function buildCharacterBody(args) {
|
|
106
120
|
return withoutAbsentFlags({
|
|
107
121
|
name: args.name,
|
|
@@ -109,6 +123,7 @@ export function buildCharacterBody(args) {
|
|
|
109
123
|
applyToPlayer: args.applyToPlayer,
|
|
110
124
|
headScale: parseNumberFlag('head-scale', args.headScale, { min: 50, max: 300 }),
|
|
111
125
|
useLibrary: args.library,
|
|
126
|
+
voxelize: args.voxelize,
|
|
112
127
|
});
|
|
113
128
|
}
|
|
114
129
|
// ── animation ───────────────────────────────────────────────────────────────
|
|
@@ -253,6 +268,44 @@ export function buildAnimationBody(args, readSpec) {
|
|
|
253
268
|
spec,
|
|
254
269
|
});
|
|
255
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* `--video-prompt` is `--video` with the clip generated instead of filmed, so it
|
|
273
|
+
* shares every downstream flag and refuses the same combinations. The extra
|
|
274
|
+
* refusal is the duration floor: `generate video` will happily make a 1 s clip,
|
|
275
|
+
* and Uthana will then reject it, which would bill the creator for footage that
|
|
276
|
+
* never had a chance.
|
|
277
|
+
*/
|
|
278
|
+
export function buildAnimationFromPromptRun(args) {
|
|
279
|
+
if (args.spec !== undefined || args.family !== undefined) {
|
|
280
|
+
throw new CliError('--video-prompt cannot be combined with --spec or --family: a captured clip is not authored from a spec.');
|
|
281
|
+
}
|
|
282
|
+
if (args.video !== undefined) {
|
|
283
|
+
throw new CliError('--video and --video-prompt cannot be combined: pass a clip you have, or describe one to generate.');
|
|
284
|
+
}
|
|
285
|
+
if (args.videoPrompt.trim() === '') {
|
|
286
|
+
throw new CliError('--video-prompt needs a description of the motion, e.g. "a person waving both arms overhead".');
|
|
287
|
+
}
|
|
288
|
+
const durationSeconds = args.videoDuration ?? MOTION_SOURCE_DEFAULT_SECONDS;
|
|
289
|
+
if (!Number.isFinite(durationSeconds)
|
|
290
|
+
|| durationSeconds < MOTION_SOURCE_MIN_SECONDS || durationSeconds > VIDEO_MAX_SECONDS) {
|
|
291
|
+
throw new CliError(`--video-duration must be ${MOTION_SOURCE_MIN_SECONDS}-${VIDEO_MAX_SECONDS} seconds; `
|
|
292
|
+
+ `got ${JSON.stringify(String(args.videoDuration))}. Video-to-motion needs at least `
|
|
293
|
+
+ `${MOTION_SOURCE_MIN_SECONDS}s of footage to read.`);
|
|
294
|
+
}
|
|
295
|
+
// The clip half is validated by the same rules --video uses. No videoPath and
|
|
296
|
+
// no videoUrl yet: the first leg fills videoUrl in once it has produced one.
|
|
297
|
+
return { description: args.videoPrompt.trim(), durationSeconds, run: buildClipFlags(args) };
|
|
298
|
+
}
|
|
299
|
+
/** `--video` takes either a local file or an already-public clip URL. */
|
|
300
|
+
export function isHttpUrl(value) {
|
|
301
|
+
try {
|
|
302
|
+
const { protocol } = new URL(value);
|
|
303
|
+
return protocol === 'http:' || protocol === 'https:';
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
256
309
|
/**
|
|
257
310
|
* `--video` is a different generator from the authored spec: the clip comes from a person, not
|
|
258
311
|
* a parameter file, so the two flag sets cannot be combined and the spec flags are refused here
|
|
@@ -262,6 +315,20 @@ export function buildAnimationVideoRun(args) {
|
|
|
262
315
|
if (args.spec !== undefined || args.family !== undefined) {
|
|
263
316
|
throw new CliError('--video cannot be combined with --spec or --family: a video clip is not authored from a spec.');
|
|
264
317
|
}
|
|
318
|
+
if (args.videoPrompt !== undefined) {
|
|
319
|
+
throw new CliError('--video and --video-prompt cannot be combined: pass a clip you have, or describe one to generate.');
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
// A URL is handed straight to the job; only a local file needs uploading.
|
|
323
|
+
...(isHttpUrl(args.video) ? { videoUrl: args.video } : { videoPath: args.video }),
|
|
324
|
+
...buildClipFlags(args),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The flags that describe the CLIP rather than where it came from, shared by
|
|
329
|
+
* `--video` and `--video-prompt` so the two lanes cannot drift apart.
|
|
330
|
+
*/
|
|
331
|
+
function buildClipFlags(args) {
|
|
265
332
|
if (args.slot !== undefined && !isLocomotionState(args.slot)) {
|
|
266
333
|
throw new CliError(`--slot must be one of ${LOCOMOTION_STATES.join(', ')}; got ${JSON.stringify(args.slot)}.`);
|
|
267
334
|
}
|
|
@@ -283,7 +350,6 @@ export function buildAnimationVideoRun(args) {
|
|
|
283
350
|
}
|
|
284
351
|
}
|
|
285
352
|
return {
|
|
286
|
-
videoPath: args.video,
|
|
287
353
|
...(args.name !== undefined ? { name: args.name } : {}),
|
|
288
354
|
...(args.model !== undefined ? { model: args.model } : {}),
|
|
289
355
|
...(args.loop !== undefined ? { loop: args.loop } : {}),
|
|
@@ -623,6 +689,37 @@ const MATERIALS_FLAG = {
|
|
|
623
689
|
description: 'Decide what the asset is made of after the bake, so metal shines and glass '
|
|
624
690
|
+ 'reflects. Pass --no-materials to leave it plain; `bitmagic assets materials` edits it later.',
|
|
625
691
|
};
|
|
692
|
+
/**
|
|
693
|
+
* The mesh-KEEPING pair, `generate prop|model`'s answer to `assets add --keep-glb` / `--voxelize`.
|
|
694
|
+
* Neither is needed in the common case: a low-poly project (game.json `artStyle`) keeps, a voxel
|
|
695
|
+
* project bakes. They exist for the one prop that goes the other way.
|
|
696
|
+
*/
|
|
697
|
+
const KEEP_GLB_FLAG = {
|
|
698
|
+
type: 'boolean',
|
|
699
|
+
description: 'Keep the generated mesh as a polygon-mesh asset instead of voxelizing it '
|
|
700
|
+
+ '(the default when game.json artStyle is low-poly). Implies --mesh; no browser is needed.',
|
|
701
|
+
};
|
|
702
|
+
const VOXELIZE_FLAG = {
|
|
703
|
+
type: 'boolean',
|
|
704
|
+
description: 'Voxelize even in a low-poly project.',
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Whether a generated mesh is kept: the flags when given, else the project's art style. Says so
|
|
708
|
+
* only when the style decided, so a voxel project's transcript is unchanged.
|
|
709
|
+
*/
|
|
710
|
+
function resolveKeepGlb(root, options, log) {
|
|
711
|
+
if (options.keepGlb === true && options.voxelize === true) {
|
|
712
|
+
throw new CliError('Pass either --keep-glb or --voxelize, not both. world.json is unchanged.');
|
|
713
|
+
}
|
|
714
|
+
if (options.keepGlb === true)
|
|
715
|
+
return true;
|
|
716
|
+
if (options.voxelize === true)
|
|
717
|
+
return false;
|
|
718
|
+
if (resolveArtStyle(root, log) !== 'low-poly')
|
|
719
|
+
return false;
|
|
720
|
+
log('Keeping the mesh: game.json artStyle is low-poly (pass --voxelize to bake it instead).');
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
626
723
|
/** The voxel path's `--glb-url`: a paid-for master to bake, instead of buying another. */
|
|
627
724
|
const MASTER_URL_FLAG = {
|
|
628
725
|
type: 'string',
|
|
@@ -646,9 +743,10 @@ function classifyGeneratedAsset(input) {
|
|
|
646
743
|
* or an asset with no `fitBox` costs nothing.
|
|
647
744
|
*/
|
|
648
745
|
export async function runPropGeneration(options) {
|
|
649
|
-
assertVoxelGridAllowed(options.voxelGrid, options.mesh === true);
|
|
746
|
+
assertVoxelGridAllowed(options.voxelGrid, options.mesh === true || options.keepGlb === true);
|
|
650
747
|
assertOneRetrySource(options.glbUrl, options.masterUrl);
|
|
651
748
|
const { output, runDeps, context, environment, token } = await resolveRun(options.json, options.deps);
|
|
749
|
+
const keepGlb = resolveKeepGlb(context.root, options, runDeps.log);
|
|
652
750
|
const result = await withJobRecord({
|
|
653
751
|
root: context.root,
|
|
654
752
|
kind: 'prop',
|
|
@@ -670,6 +768,8 @@ export async function runPropGeneration(options) {
|
|
|
670
768
|
...(options.masterUrl ? { masterUrl: options.masterUrl } : {}),
|
|
671
769
|
...(options.mesh === true ? { mesh: true } : {}),
|
|
672
770
|
...(options.voxelGrid !== undefined ? { voxelGrid: options.voxelGrid } : {}),
|
|
771
|
+
...(keepGlb ? { keepGlb: true } : {}),
|
|
772
|
+
...(runDeps.keepGlbUpload !== undefined ? { keepGlbUpload: runDeps.keepGlbUpload } : {}),
|
|
673
773
|
fetchImpl: runDeps.fetch,
|
|
674
774
|
log,
|
|
675
775
|
});
|
|
@@ -680,12 +780,13 @@ export async function runPropGeneration(options) {
|
|
|
680
780
|
});
|
|
681
781
|
runDeps.log(`Upgraded "${result.assetName}" — ${result.instanceCount} placed instance`
|
|
682
782
|
+ `${result.instanceCount === 1 ? '' : 's'} now use the generated version.`);
|
|
683
|
-
|
|
783
|
+
// Materials name what each colour group of a VOXEL asset is made of; a kept mesh has its own.
|
|
784
|
+
const materials = options.materials === false || keepGlb
|
|
684
785
|
? null
|
|
685
786
|
: await classifyGeneratedAsset({ context, environment, token, assetId: result.assetId, runDeps });
|
|
686
787
|
// `result` already carries glbUrl, so a retry after a later failure can find the paid-for mesh
|
|
687
788
|
// in the JSON document rather than only in the error text.
|
|
688
|
-
output.result({ ok: true, kind: 'prop', ...result, ...(materials === null ? {} : { materials }) });
|
|
789
|
+
output.result({ ok: true, kind: 'prop', ...result, ...(keepGlb ? { keptMesh: true } : {}), ...(materials === null ? {} : { materials }) });
|
|
689
790
|
runDeps.log(DEV_VIEW_RELOADS);
|
|
690
791
|
return result;
|
|
691
792
|
}
|
|
@@ -699,6 +800,14 @@ export async function runPropGeneration(options) {
|
|
|
699
800
|
* prompt nor a preset, which is refused here before anything is spent.
|
|
700
801
|
*/
|
|
701
802
|
export async function runVehicleGeneration(options) {
|
|
803
|
+
{
|
|
804
|
+
// A vehicle is voxel-native (its builder and finishes assume six face normals), so a low-poly
|
|
805
|
+
// project gets one anyway — said once, so the mismatch is a known choice rather than a surprise.
|
|
806
|
+
const root = findProjectRootOrNull();
|
|
807
|
+
if (root !== null && resolveArtStyle(root) === 'low-poly') {
|
|
808
|
+
process.stderr.write('note: vehicles are always voxelized; this low-poly project (game.json artStyle) will get a voxel car.\n');
|
|
809
|
+
}
|
|
810
|
+
}
|
|
702
811
|
if (options.prompt === undefined && options.presetName === undefined && options.glbUrl === undefined) {
|
|
703
812
|
throw new CliError('Provide --prompt to design a vehicle, or --preset to build a standard one. '
|
|
704
813
|
+ 'Run `bitmagic generate vehicle --preset ?` to see the preset names.');
|
|
@@ -759,9 +868,10 @@ export async function runVehicleGeneration(options) {
|
|
|
759
868
|
* has no instances yet and scales to `--height` (or the mesh's own size) instead.
|
|
760
869
|
*/
|
|
761
870
|
export async function runModelGeneration(options) {
|
|
762
|
-
assertVoxelGridAllowed(options.voxelGrid, options.mesh === true);
|
|
871
|
+
assertVoxelGridAllowed(options.voxelGrid, options.mesh === true || options.keepGlb === true);
|
|
763
872
|
assertOneRetrySource(options.glbUrl, options.masterUrl);
|
|
764
873
|
const { output, runDeps, context, environment, token } = await resolveRun(options.json, options.deps);
|
|
874
|
+
const keepGlb = resolveKeepGlb(context.root, options, runDeps.log);
|
|
765
875
|
const result = await withJobRecord({
|
|
766
876
|
root: context.root,
|
|
767
877
|
kind: 'model',
|
|
@@ -790,6 +900,8 @@ export async function runModelGeneration(options) {
|
|
|
790
900
|
},
|
|
791
901
|
fetchImpl: runDeps.fetch,
|
|
792
902
|
...(runDeps.voxelize !== undefined ? { voxelize: runDeps.voxelize } : {}),
|
|
903
|
+
...(keepGlb ? { keepGlb: true } : {}),
|
|
904
|
+
...(runDeps.keepGlbUpload !== undefined ? { keepGlbUpload: runDeps.keepGlbUpload } : {}),
|
|
793
905
|
log,
|
|
794
906
|
});
|
|
795
907
|
}
|
|
@@ -798,9 +910,11 @@ export async function runModelGeneration(options) {
|
|
|
798
910
|
}
|
|
799
911
|
});
|
|
800
912
|
runDeps.log(`Added "${result.assetName}" (id ${result.assetId})`
|
|
801
|
-
+ `${result.voxelCount !== undefined ? `, ${result.voxelCount} voxels` : ''}
|
|
913
|
+
+ `${result.voxelCount !== undefined ? `, ${result.voxelCount} voxels` : ''}`
|
|
914
|
+
+ `${result.keptMesh === true ? ', kept as a polygon mesh' : ''}. Place it from game `
|
|
802
915
|
+ 'code, or drag it in from the dev view\'s Add-object palette.');
|
|
803
|
-
|
|
916
|
+
// Materials name what each colour group of a VOXEL asset is made of; a kept mesh has its own.
|
|
917
|
+
const materials = options.materials === false || keepGlb
|
|
804
918
|
? null
|
|
805
919
|
: await classifyGeneratedAsset({ context, environment, token, assetId: result.assetId, runDeps });
|
|
806
920
|
// `result` carries whichever source was paid for — glbUrl or masterUrl — so a retry after a
|
|
@@ -1100,6 +1214,11 @@ const characterCommand = defineCommand({
|
|
|
1100
1214
|
type: 'boolean',
|
|
1101
1215
|
negativeDescription: 'Always make a new character; do not use a ready-made one from the library.',
|
|
1102
1216
|
},
|
|
1217
|
+
voxelize: {
|
|
1218
|
+
type: 'boolean',
|
|
1219
|
+
description: 'Voxelize the rigged body. Omit to follow game.json artStyle: voxel games voxelize, low-poly games keep the textured rig.',
|
|
1220
|
+
negativeDescription: 'Keep the textured low-poly rig instead of voxelizing it.',
|
|
1221
|
+
},
|
|
1103
1222
|
'original-prompt': ORIGINAL_PROMPT_FLAG,
|
|
1104
1223
|
json: JSON_FLAG,
|
|
1105
1224
|
},
|
|
@@ -1110,6 +1229,7 @@ const characterCommand = defineCommand({
|
|
|
1110
1229
|
applyToPlayer: args['apply-to-player'],
|
|
1111
1230
|
headScale: args['head-scale'],
|
|
1112
1231
|
library: args.library,
|
|
1232
|
+
voxelize: args.voxelize ?? defaultVoxelizeForProject(),
|
|
1113
1233
|
});
|
|
1114
1234
|
await runAssetGeneration('character', 'character', withParent(body), args.json === true);
|
|
1115
1235
|
},
|
|
@@ -1164,25 +1284,40 @@ const animationCommand = defineCommand({
|
|
|
1164
1284
|
video: {
|
|
1165
1285
|
type: 'string',
|
|
1166
1286
|
description: `Make the clip from a video of one person instead of a spec (Uthana video-to-motion, costs sparks). `
|
|
1167
|
-
+
|
|
1287
|
+
+ `A local ${VIDEO_EXTENSIONS.map((e) => `.${e}`).join('/')} file, or an http(s) URL of one already uploaded. `
|
|
1288
|
+
+ `2-60 s, full body in frame. Cannot be combined with --spec/--family/--video-prompt.`,
|
|
1289
|
+
},
|
|
1290
|
+
'video-prompt': {
|
|
1291
|
+
type: 'string',
|
|
1292
|
+
description: 'Describe a motion and have the source clip generated instead of filmed '
|
|
1293
|
+
+ '(costs sparks for both the clip and the motion). The clip is framed automatically for '
|
|
1294
|
+
+ 'capture: one person, whole body in shot, static camera. Cannot be combined with '
|
|
1295
|
+
+ '--video/--spec/--family.',
|
|
1296
|
+
},
|
|
1297
|
+
'video-duration': {
|
|
1298
|
+
type: 'string',
|
|
1299
|
+
description: `Length of the generated source clip in seconds, ${MOTION_SOURCE_MIN_SECONDS}-${VIDEO_MAX_SECONDS} `
|
|
1300
|
+
+ `(default ${MOTION_SOURCE_DEFAULT_SECONDS}); with --video-prompt. Billed per second. Not --duration, which is `
|
|
1301
|
+
+ 'the authored-spec clip length.',
|
|
1168
1302
|
},
|
|
1169
1303
|
model: {
|
|
1170
1304
|
type: 'string',
|
|
1171
|
-
description: `Video-to-motion model (with --video): ${VIDEO_TO_MOTION_MODELS.join(' | ')}.`,
|
|
1305
|
+
description: `Video-to-motion model (with --video or --video-prompt): ${VIDEO_TO_MOTION_MODELS.join(' | ')}.`,
|
|
1172
1306
|
},
|
|
1173
1307
|
loop: {
|
|
1174
1308
|
type: 'boolean',
|
|
1175
|
-
description: 'Mark the clip as looping (with --video). Idle, walk and run clips should loop.',
|
|
1309
|
+
description: 'Mark the clip as looping (with --video/--video-prompt). Idle, walk and run clips should loop.',
|
|
1176
1310
|
},
|
|
1177
1311
|
slot: {
|
|
1178
1312
|
type: 'string',
|
|
1179
|
-
description: `Pin the clip to a locomotion state for the player and every NPC (with --video): ${LOCOMOTION_STATES.join(' | ')}. `
|
|
1313
|
+
description: `Pin the clip to a locomotion state for the player and every NPC (with --video/--video-prompt): ${LOCOMOTION_STATES.join(' | ')}. `
|
|
1180
1314
|
+ 'Omit for a one-shot clip played with playCustomAnimation().',
|
|
1181
1315
|
},
|
|
1182
1316
|
'auto-trim': {
|
|
1183
1317
|
type: 'boolean',
|
|
1184
1318
|
description: 'Cut "walking to the camera" frames off both ends of the video before the motion is made (with --video). '
|
|
1185
|
-
+ 'Useful for clips recorded webcam-style; the dev-view recorder instead confirms an explicit window.'
|
|
1319
|
+
+ 'Useful for clips recorded webcam-style; the dev-view recorder instead confirms an explicit window. '
|
|
1320
|
+
+ 'A generated --video-prompt clip has no such frames, so it rarely needs this.',
|
|
1186
1321
|
},
|
|
1187
1322
|
'trim-start': {
|
|
1188
1323
|
type: 'string',
|
|
@@ -1197,6 +1332,25 @@ const animationCommand = defineCommand({
|
|
|
1197
1332
|
json: JSON_FLAG,
|
|
1198
1333
|
},
|
|
1199
1334
|
async run({ args }) {
|
|
1335
|
+
if (args['video-prompt'] !== undefined) {
|
|
1336
|
+
const run = buildAnimationFromPromptRun({
|
|
1337
|
+
videoPrompt: args['video-prompt'],
|
|
1338
|
+
videoDuration: args['video-duration'] !== undefined ? Number(args['video-duration']) : undefined,
|
|
1339
|
+
video: args.video,
|
|
1340
|
+
name: args.name,
|
|
1341
|
+
model: args.model,
|
|
1342
|
+
loop: args.loop === true ? true : undefined,
|
|
1343
|
+
slot: args.slot,
|
|
1344
|
+
autoTrim: args['auto-trim'] === true ? true : undefined,
|
|
1345
|
+
trimStart: args['trim-start'] !== undefined ? Number(args['trim-start']) : undefined,
|
|
1346
|
+
trimEnd: args['trim-end'] !== undefined ? Number(args['trim-end']) : undefined,
|
|
1347
|
+
assetId: args['asset-id'],
|
|
1348
|
+
spec: args.spec,
|
|
1349
|
+
family: args.family,
|
|
1350
|
+
});
|
|
1351
|
+
await runAnimationFromPrompt(run, args.json === true, undefined, creatorPromptField(args).creatorPrompt);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1200
1354
|
if (args.video !== undefined) {
|
|
1201
1355
|
const run = buildAnimationVideoRun({
|
|
1202
1356
|
video: args.video,
|
|
@@ -1236,7 +1390,7 @@ const animationCommand = defineCommand({
|
|
|
1236
1390
|
*/
|
|
1237
1391
|
export async function runAnimationFromVideo(run, json, deps) {
|
|
1238
1392
|
const { output, runDeps, context, environment, token } = await resolveRun(json, deps);
|
|
1239
|
-
const label = run.name ?? path.basename(run.videoPath);
|
|
1393
|
+
const label = run.name ?? (run.videoPath !== undefined ? path.basename(run.videoPath) : 'Recorded motion');
|
|
1240
1394
|
const result = await withJobRecord({
|
|
1241
1395
|
root: context.root, kind: 'animation-video', label, log: runDeps.log,
|
|
1242
1396
|
}, async (log) => {
|
|
@@ -1272,6 +1426,85 @@ export async function runAnimationFromVideo(run, json, deps) {
|
|
|
1272
1426
|
});
|
|
1273
1427
|
runDeps.log(DEV_VIEW_RELOADS);
|
|
1274
1428
|
}
|
|
1429
|
+
/**
|
|
1430
|
+
* The `--video-prompt` lane: text -> clip -> motion, the two existing job lanes run back to back.
|
|
1431
|
+
*
|
|
1432
|
+
* There is no chained endpoint behind this — asset-forger has one route that makes a video and
|
|
1433
|
+
* another that reads one, and this is the seam between them. Both legs bill under their own
|
|
1434
|
+
* existing key (`generate-video` per second, `generate-animation-from-video` flat), the way
|
|
1435
|
+
* design-vehicle prices its chained image sub-generations rather than minting a composite.
|
|
1436
|
+
*
|
|
1437
|
+
* Both asset rows are kept. The clip is a real `type: 'video'` asset the creator can play back
|
|
1438
|
+
* or reuse as a cutscene, and keeping it is also what makes a leg-2 failure recoverable: the
|
|
1439
|
+
* footage is already paid for, so the error hands back a `--video <url>` retry instead of asking
|
|
1440
|
+
* for the whole thing again.
|
|
1441
|
+
*/
|
|
1442
|
+
export async function runAnimationFromPrompt(prompt, json, deps, creatorPrompt) {
|
|
1443
|
+
const { output, runDeps, context, environment, token } = await resolveRun(json, deps);
|
|
1444
|
+
const label = prompt.run.name ?? prompt.description;
|
|
1445
|
+
const video = await withJobRecord({
|
|
1446
|
+
root: context.root, kind: 'video', label: `${label} (motion source)`, log: runDeps.log,
|
|
1447
|
+
}, async (log) => {
|
|
1448
|
+
try {
|
|
1449
|
+
return await generateAndInstallVideo({
|
|
1450
|
+
context, environment, token,
|
|
1451
|
+
run: {
|
|
1452
|
+
prompt: buildMotionSourcePrompt(prompt.description),
|
|
1453
|
+
durationSeconds: prompt.durationSeconds,
|
|
1454
|
+
name: `${label} (motion source)`,
|
|
1455
|
+
...(creatorPrompt !== undefined ? { creatorPrompt } : {}),
|
|
1456
|
+
},
|
|
1457
|
+
deps: { fetch: runDeps.fetch, sleep: runDeps.sleep, log },
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
throw decoratedError(error, (message) => message);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
runDeps.log(`Source clip generated (${video.durationSeconds.toFixed(1)}s): ${video.videoUrl}`);
|
|
1465
|
+
const motion = await withJobRecord({
|
|
1466
|
+
root: context.root, kind: 'animation-video', label, log: runDeps.log,
|
|
1467
|
+
}, async (log) => {
|
|
1468
|
+
try {
|
|
1469
|
+
return await generateAndInstallAnimationFromVideo({
|
|
1470
|
+
context, environment, token,
|
|
1471
|
+
run: { ...prompt.run, videoUrl: video.videoUrl, name: label },
|
|
1472
|
+
deps: { fetch: runDeps.fetch, sleep: runDeps.sleep, log },
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
// The clip survived and is already an asset, so say what it costs to pick
|
|
1477
|
+
// up from here rather than letting the creator re-buy the footage.
|
|
1478
|
+
throw decoratedError(error, (message) => `${message}\n`
|
|
1479
|
+
+ `The source clip was kept as asset "${video.assetId}". Retry the motion without `
|
|
1480
|
+
+ `regenerating it:\n bitmagic generate animation --video ${video.videoUrl} --name ${JSON.stringify(label)}`);
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
const sparksCharged = video.sparksCharged + motion.sparksCharged;
|
|
1484
|
+
const patchesApplied = video.patchesApplied + motion.patchesApplied;
|
|
1485
|
+
runDeps.log(`Generated animation "${label}" from a generated clip (${motion.durationSeconds.toFixed(1)}s)${describeAutoTrim(motion)}: ${motion.animationUrl}`);
|
|
1486
|
+
runDeps.log(` - motionId ${motion.motionId}${motion.locomotionState ? ` — pinned to ${motion.locomotionState}` : ''}`);
|
|
1487
|
+
runDeps.log(` - source clip asset ${video.assetId}`);
|
|
1488
|
+
runDeps.log(`world.json updated (${patchesApplied} patch${patchesApplied === 1 ? '' : 'es'}).`);
|
|
1489
|
+
if (!motion.locomotionState) {
|
|
1490
|
+
runDeps.log(`Play it with playCustomAnimation("${motion.motionId}"), or re-run with --slot idle|walk|run to make it locomotion.`);
|
|
1491
|
+
}
|
|
1492
|
+
output.result({
|
|
1493
|
+
ok: true,
|
|
1494
|
+
kind: 'animation',
|
|
1495
|
+
motionId: motion.motionId,
|
|
1496
|
+
animationUrl: motion.animationUrl,
|
|
1497
|
+
durationSeconds: motion.durationSeconds,
|
|
1498
|
+
sourceVideoAssetId: video.assetId,
|
|
1499
|
+
sourceVideoUrl: video.videoUrl,
|
|
1500
|
+
...(motion.trimmedStartSeconds !== undefined ? { trimmedStartSeconds: motion.trimmedStartSeconds } : {}),
|
|
1501
|
+
...(motion.trimmedEndSeconds !== undefined ? { trimmedEndSeconds: motion.trimmedEndSeconds } : {}),
|
|
1502
|
+
...(motion.locomotionState ? { locomotionState: motion.locomotionState } : {}),
|
|
1503
|
+
sparksCharged,
|
|
1504
|
+
patchesApplied,
|
|
1505
|
+
});
|
|
1506
|
+
runDeps.log(DEV_VIEW_RELOADS);
|
|
1507
|
+
}
|
|
1275
1508
|
const videoCommand = defineCommand({
|
|
1276
1509
|
meta: {
|
|
1277
1510
|
name: 'video',
|
|
@@ -1304,6 +1537,12 @@ const videoCommand = defineCommand({
|
|
|
1304
1537
|
type: 'string',
|
|
1305
1538
|
description: 'Pre-minted asset_… id to store the clip under, so code already referencing it resolves.',
|
|
1306
1539
|
},
|
|
1540
|
+
'contact-sheet': {
|
|
1541
|
+
type: 'boolean',
|
|
1542
|
+
default: true,
|
|
1543
|
+
description: 'Grab 8 frames into a PNG under .bitmagic/videos/ so the clip can be reviewed (the default). '
|
|
1544
|
+
+ 'Pass --no-contact-sheet to skip the download; needs ffmpeg.',
|
|
1545
|
+
},
|
|
1307
1546
|
'original-prompt': ORIGINAL_PROMPT_FLAG,
|
|
1308
1547
|
json: JSON_FLAG,
|
|
1309
1548
|
},
|
|
@@ -1321,15 +1560,15 @@ const videoCommand = defineCommand({
|
|
|
1321
1560
|
...(args['asset-id'] !== undefined ? { assetId: args['asset-id'] } : {}),
|
|
1322
1561
|
...creatorPromptField(args),
|
|
1323
1562
|
};
|
|
1324
|
-
await runVideoGeneration(run, args.json === true);
|
|
1563
|
+
await runVideoGeneration(run, args.json === true, { contactSheet: args['contact-sheet'] !== false });
|
|
1325
1564
|
},
|
|
1326
1565
|
});
|
|
1327
1566
|
/**
|
|
1328
1567
|
* Job-shaped rather than streamed (see generate/video.ts), so it cannot share `runAssetGeneration`;
|
|
1329
1568
|
* it does share the job record, so the dev view shows it beside every other generation.
|
|
1330
1569
|
*/
|
|
1331
|
-
export async function runVideoGeneration(run, json,
|
|
1332
|
-
const { output, runDeps, context, environment, token } = await resolveRun(json, deps);
|
|
1570
|
+
export async function runVideoGeneration(run, json, options = {}) {
|
|
1571
|
+
const { output, runDeps, context, environment, token } = await resolveRun(json, options.deps);
|
|
1333
1572
|
const label = run.name ?? run.prompt;
|
|
1334
1573
|
const result = await withJobRecord({
|
|
1335
1574
|
root: context.root, kind: 'video', label, log: runDeps.log,
|
|
@@ -1349,7 +1588,32 @@ export async function runVideoGeneration(run, json, deps) {
|
|
|
1349
1588
|
: '';
|
|
1350
1589
|
runDeps.log(`Generated video "${label}" (${result.durationSeconds.toFixed(1)}s${shape}): ${result.videoUrl}`);
|
|
1351
1590
|
runDeps.log(`world.json updated (${result.patchesApplied} patch${result.patchesApplied === 1 ? '' : 'es'}).`);
|
|
1352
|
-
|
|
1591
|
+
const review = options.contactSheet === false
|
|
1592
|
+
? null
|
|
1593
|
+
: await reviewGeneratedClip({
|
|
1594
|
+
root: context.root,
|
|
1595
|
+
assetId: result.assetId,
|
|
1596
|
+
videoUrl: result.videoUrl,
|
|
1597
|
+
durationSeconds: result.durationSeconds,
|
|
1598
|
+
fetch: runDeps.fetch,
|
|
1599
|
+
});
|
|
1600
|
+
if (review !== null) {
|
|
1601
|
+
if (review.sheetPath !== null)
|
|
1602
|
+
runDeps.log(`Frames: ${review.sheetPath}`);
|
|
1603
|
+
// Silence is what decides whether the cutscene needs a music bed under it, and it is
|
|
1604
|
+
// invisible in the frames — so it is said in words either way, never left to be assumed.
|
|
1605
|
+
if (review.hasAudioTrack === false) {
|
|
1606
|
+
runDeps.log('The clip has NO audio track — play a music bed under it if the cutscene needs sound.');
|
|
1607
|
+
}
|
|
1608
|
+
else if (review.hasAudioTrack === true) {
|
|
1609
|
+
runDeps.log('The clip carries its own audio track.');
|
|
1610
|
+
}
|
|
1611
|
+
for (const note of review.notes)
|
|
1612
|
+
runDeps.log(` ${note}`);
|
|
1613
|
+
}
|
|
1614
|
+
runDeps.log(`Play it with engine.playVideo("${result.assetId}") — from the Play click or later, `
|
|
1615
|
+
+ 'never before: without a prior user gesture the browser blocks autoplay and the clip silently '
|
|
1616
|
+
+ 'does not run. See video-cutscenes.md.');
|
|
1353
1617
|
output.result({
|
|
1354
1618
|
ok: true,
|
|
1355
1619
|
kind: 'video',
|
|
@@ -1361,6 +1625,8 @@ export async function runVideoGeneration(run, json, deps) {
|
|
|
1361
1625
|
...(result.height !== undefined ? { height: result.height } : {}),
|
|
1362
1626
|
sparksCharged: result.sparksCharged,
|
|
1363
1627
|
patchesApplied: result.patchesApplied,
|
|
1628
|
+
...(review?.sheetPath != null ? { contactSheetPath: review.sheetPath } : {}),
|
|
1629
|
+
...(review?.hasAudioTrack != null ? { hasAudioTrack: review.hasAudioTrack } : {}),
|
|
1364
1630
|
});
|
|
1365
1631
|
runDeps.log(DEV_VIEW_RELOADS);
|
|
1366
1632
|
}
|
|
@@ -1386,6 +1652,8 @@ const propCommand = defineCommand({
|
|
|
1386
1652
|
},
|
|
1387
1653
|
'master-url': MASTER_URL_FLAG,
|
|
1388
1654
|
mesh: MESH_FLAG,
|
|
1655
|
+
'keep-glb': KEEP_GLB_FLAG,
|
|
1656
|
+
voxelize: VOXELIZE_FLAG,
|
|
1389
1657
|
'direct-voxels': DIRECT_VOXELS_FLAG,
|
|
1390
1658
|
'voxel-grid': VOXEL_GRID_FLAG,
|
|
1391
1659
|
materials: MATERIALS_FLAG,
|
|
@@ -1399,6 +1667,8 @@ const propCommand = defineCommand({
|
|
|
1399
1667
|
...(args['glb-url'] ? { glbUrl: args['glb-url'] } : {}),
|
|
1400
1668
|
...(args['master-url'] ? { masterUrl: args['master-url'] } : {}),
|
|
1401
1669
|
...(args.mesh === true ? { mesh: true } : {}),
|
|
1670
|
+
...(args['keep-glb'] === true ? { keepGlb: true } : {}),
|
|
1671
|
+
...(args.voxelize === true ? { voxelize: true } : {}),
|
|
1402
1672
|
...voxelGridField(args['voxel-grid']),
|
|
1403
1673
|
...(args.materials === false ? { materials: false } : {}),
|
|
1404
1674
|
json: args.json === true,
|
|
@@ -1475,7 +1745,7 @@ const modelCommand = defineCommand({
|
|
|
1475
1745
|
},
|
|
1476
1746
|
height: {
|
|
1477
1747
|
type: 'string',
|
|
1478
|
-
description: 'Scale the model to this height in metres. Omit to keep the generated mesh\'s own size.',
|
|
1748
|
+
description: 'Scale the model to this height in metres. Omit to keep the generated mesh\'s own size (2 m when the mesh is kept).',
|
|
1479
1749
|
},
|
|
1480
1750
|
hollow: {
|
|
1481
1751
|
type: 'boolean',
|
|
@@ -1491,6 +1761,8 @@ const modelCommand = defineCommand({
|
|
|
1491
1761
|
},
|
|
1492
1762
|
'master-url': MASTER_URL_FLAG,
|
|
1493
1763
|
mesh: MESH_FLAG,
|
|
1764
|
+
'keep-glb': KEEP_GLB_FLAG,
|
|
1765
|
+
voxelize: VOXELIZE_FLAG,
|
|
1494
1766
|
'direct-voxels': DIRECT_VOXELS_FLAG,
|
|
1495
1767
|
'voxel-grid': VOXEL_GRID_FLAG,
|
|
1496
1768
|
materials: MATERIALS_FLAG,
|
|
@@ -1523,6 +1795,8 @@ const modelCommand = defineCommand({
|
|
|
1523
1795
|
...(args['glb-url'] !== undefined ? { glbUrl: args['glb-url'] } : {}),
|
|
1524
1796
|
...(args['master-url'] !== undefined ? { masterUrl: args['master-url'] } : {}),
|
|
1525
1797
|
...(args.mesh === true ? { mesh: true } : {}),
|
|
1798
|
+
...(args['keep-glb'] === true ? { keepGlb: true } : {}),
|
|
1799
|
+
...(args.voxelize === true ? { voxelize: true } : {}),
|
|
1526
1800
|
...voxelGridField(args['voxel-grid']),
|
|
1527
1801
|
...(args.materials === false ? { materials: false } : {}),
|
|
1528
1802
|
json: args.json === true,
|