feat(generation): add Pixal3D generator to supported ecosystems

Wire Pixal3D as a new generation ecosystem, enabling it in the generation form and orchestration pipeline. Creates orchestrator handler, graph configuration, and ecosystem integration following the established patterns from Meshy and Tripio ecosystems. Updates @civitai/client to beta.95 to support the new model type.
This commit is contained in:
Luis Rojas
2026-08-28 18:47:33 -04:00
parent 89e18fe73a
commit cc6bf7babc
17 changed files with 426 additions and 16 deletions
+1 -1
View File
@@ -125,7 +125,7 @@
"@civitai/app-sdk": "^0.14.0",
"@civitai/auth": "workspace:*",
"@civitai/buzz": "workspace:*",
"@civitai/client": "0.2.0-beta.94",
"@civitai/client": "0.2.0-beta.95",
"@civitai/cybertipline-tools": "^0.1.0",
"@civitai/db-queries": "workspace:*",
"@civitai/db-schema": "workspace:*",
@@ -211,6 +211,8 @@ export const ECO = {
PolyGen: 73,
Tripo: 75,
Hunyuan3D: 76,
Pixal3D: 83,
Trellis2: 84,
// Utility ecosystems
Upscaler: 66,
@@ -854,6 +856,18 @@ export const ecosystems: EcosystemRecord[] = [
displayName: 'Hunyuan3D',
sortOrder: 303,
},
{
id: ECO.Pixal3D,
key: 'Pixal3D',
displayName: 'Pixal3D',
sortOrder: 304,
},
{
id: ECO.Trellis2,
key: 'Trellis2',
displayName: 'Trellis.2',
sortOrder: 305,
},
];
export const ecosystemById = new Map(ecosystems.map((e) => [e.id, e]));
@@ -866,7 +880,13 @@ export const ecosystemByKey = new Map(ecosystems.map((e) => [e.key, e]));
* (their `BaseModelRecord`s register as `type: 'image'`). Single source of
* truth for the 3D ecosystem picker filter in `getEcosystemDisplayItems`.
*/
export const MODEL3D_ECOSYSTEM_IDS = new Set<number>([ECO.PolyGen, ECO.Tripo, ECO.Hunyuan3D]);
export const MODEL3D_ECOSYSTEM_IDS = new Set<number>([
ECO.PolyGen,
ECO.Tripo,
ECO.Hunyuan3D,
ECO.Pixal3D,
ECO.Trellis2,
]);
/** Ecosystem keys for the 3D-model ecosystems (`model3d` output). */
export const MODEL3D_ECOSYSTEM_KEYS = new Set<string>(
@@ -928,6 +948,9 @@ export const SELF_HOSTED_ECOSYSTEM_KEYS = [
'Ace',
// Hunyuan3dComfyPolyGenInput (3D; Meshy/Tripo are FAL and stay external)
'Hunyuan3D',
// Trellis2ImageTo3dComfyPolyGenInput (3D; Pixal3D + Trellis.2 are modelVersions of trellis2)
'Pixal3D',
'Trellis2',
] as const;
const selfHostedEcosystemKeySet = new Set<string>(SELF_HOSTED_ECOSYSTEM_KEYS);
@@ -1188,6 +1211,8 @@ export const ecosystemSupport: EcosystemSupport[] = [
// route the img2model3d workflow to these ecosystems.
{ ecosystemId: ECO.Tripo, supportType: 'generation', modelTypes: [] },
{ ecosystemId: ECO.Hunyuan3D, supportType: 'generation', modelTypes: [] },
{ ecosystemId: ECO.Pixal3D, supportType: 'generation', modelTypes: [] },
{ ecosystemId: ECO.Trellis2, supportType: 'generation', modelTypes: [] },
// Upscaler - upscaler models only
{ ecosystemId: ECO.Upscaler, supportType: 'generation', modelTypes: [ModelType.Upscaler] },
@@ -2143,6 +2168,8 @@ export const BM = {
Reve: 96,
MageFlow: 97,
Flux3Video: 98,
Pixal3D: 102,
Trellis2: 103,
} as const;
// Guard against duplicate ids — `baseModelById` is keyed by id, so collisions
@@ -3526,6 +3553,22 @@ export const baseModelRecords: BaseModelRecord[] = [
ecosystemId: ECO.Hunyuan3D,
hidden: true,
},
{
id: BM.Pixal3D,
name: 'Pixal3D',
description: 'Pixal3D image-to-3D generation (via Comfy)',
type: 'image',
ecosystemId: ECO.Pixal3D,
hidden: true,
},
{
id: BM.Trellis2,
name: 'Trellis.2',
description: 'Trellis.2 image-to-3D generation (via Comfy)',
type: 'image',
ecosystemId: ECO.Trellis2,
hidden: true,
},
];
export const baseModelById = new Map(baseModelRecords.map((m) => [m.id, m]));
+5 -5
View File
@@ -47,8 +47,8 @@ importers:
specifier: workspace:*
version: link:packages/civitai-buzz
'@civitai/client':
specifier: 0.2.0-beta.94
version: 0.2.0-beta.94
specifier: 0.2.0-beta.95
version: 0.2.0-beta.95
'@civitai/cybertipline-tools':
specifier: ^0.1.0
version: 0.1.0
@@ -2089,8 +2089,8 @@ packages:
resolution: {integrity: sha512-dSXkI8hVoozzlPS9DymzoyncWz8eapH3wkSDzDVGWjjqamXn8Dbcq1hWu5xUZkZmqCaR76F7BS9BQNqvOJ++Sw==}
engines: {git: '>=2.11.0', node: '>=18.0.0', npm: '>=7.19.0', yarn: '>=1.7.0'}
'@civitai/client@0.2.0-beta.94':
resolution: {integrity: sha512-HEP+8BK+3ovJWmIeElHKniH1Oy32JGlA0RvMKSQ2hHIO6l5Hm6Nbnq1v8yYAg8ePP3FlZHkCHyxWYQjjimbfig==}
'@civitai/client@0.2.0-beta.95':
resolution: {integrity: sha512-wuKafMnIizfd08Wg6gjTjWHrOyLkXvr9gUPLdNFP1J7SUc56K5bG44gZKYzy1vTaVrmu8PFjrJabFh9Tz4IEgA==}
engines: {git: '>=2.11.0', node: '>=18.0.0', npm: '>=7.19.0', yarn: '>=1.7.0'}
'@civitai/cybertipline-tools@0.1.0':
@@ -13270,7 +13270,7 @@ snapshots:
rfc6902: 5.2.0
tslib: 2.8.1
'@civitai/client@0.2.0-beta.94':
'@civitai/client@0.2.0-beta.95':
dependencies:
'@hey-api/client-fetch': 0.1.14
rfc6902: 5.2.0
@@ -104,6 +104,8 @@ const POLYGEN_ECOSYSTEM_MODEL_LABELS: Record<string, string> = {
PolyGen: 'Meshy',
Tripo: 'Tripo',
Hunyuan3D: 'Hunyuan3D',
Pixal3D: 'Pixal3D',
Trellis2: 'Trellis.2',
};
export function QueueItem({
@@ -484,9 +484,9 @@ function InnerProvider({
// The 3D ecosystem to land on — preserve the model (Meshy/Tripo/
// Hunyuan3D) the source was generated with. Falls back to PolyGen
// (Meshy) for legacy items whose metadata predates the model selector.
const model3dEcosystem: 'PolyGen' | 'Tripo' | 'Hunyuan3D' =
const model3dEcosystem: 'PolyGen' | 'Tripo' | 'Hunyuan3D' | 'Pixal3D' | 'Trellis2' =
remixEcosystemKey && MODEL3D_ECOSYSTEM_KEYS.has(remixEcosystemKey)
? (remixEcosystemKey as 'PolyGen' | 'Tripo' | 'Hunyuan3D')
? (remixEcosystemKey as 'PolyGen' | 'Tripo' | 'Hunyuan3D' | 'Pixal3D' | 'Trellis2')
: 'PolyGen';
let resolvedWorkflow: string;
@@ -6,7 +6,10 @@
display: grid;
gap: var(--mantine-spacing-xs);
grid-template-columns: repeat(2, minmax(0, 200px));
justify-content: center;
// Left-align with the field label. Centering pushed a lone image (every
// image-to-3D input is max:1) off to the right of the form; multi-image
// inputs simply pack from the left.
justify-content: start;
@container (min-width: 530px) {
grid-template-columns: repeat(3, minmax(0, 200px));
@@ -0,0 +1,66 @@
/**
* Pixal3D Schema (Pixal3D via Comfy / Trellis2)
*
* Zod schema for the Pixal3D 3D-model generation form. Pixal3D is a
* `modelVersion` of the Trellis2 comfy pipeline, exposed as its own ecosystem.
* Like Tripo and Hunyuan3D it is image-to-3D only, so there is no text-to-3D
* branch and no `process` discriminator.
*
* Mirrors `src/server/orchestrator/hunyuan3d/hunyuan3d.schema.ts` (the other
* comfy 3D ecosystem):
* - `sourceImageSchema` reused for the "URL or upload" pattern.
* - `toPixal3dPolyGenInput` converts validated form data to the
* `Trellis2ImageTo3dComfyPolyGenInput` shape (`engine: 'comfy',
* model: 'trellis2', operation: 'imageTo3D', modelVersion: 'pixal3D'`)
* consumed by the graph handler when building the `polyGen` step.
*
* Only a small, user-meaningful subset of the client's comfy knobs is surfaced
* (texture / remesh / pbr toggles + seed); the orchestrator applies its own
* defaults for the remaining sampler / step / resolution fields.
*/
import type { Trellis2ImageTo3dComfyPolyGenInput } from '@civitai/client';
import * as z from 'zod';
import { sourceImageSchema } from '~/server/orchestrator/infrastructure/base.schema';
import { removeEmpty } from '~/utils/object-helpers';
// =============================================================================
// Schema
// =============================================================================
export const pixal3dGenerationSchema = z.object({
sourceImage: sourceImageSchema,
shouldTexture: z.boolean().default(true),
shouldRemesh: z.boolean().default(true),
enablePbr: z.boolean().default(false),
// Comfy accepts a 32-bit signed int seed; optional so the handler can
// randomize when omitted.
seed: z.number().int().min(-2147483648).max(2147483647).optional(),
});
export type Pixal3dGenerationSchema = z.infer<typeof pixal3dGenerationSchema>;
// =============================================================================
// Helper — convert validated schema to Pixal3D/Comfy PolyGen input shape
// =============================================================================
/**
* Convert form data to a Pixal3D/Comfy PolyGen input. Used by the graph handler
* when building the `PolyGenStepTemplate` for `submitWorkflow`. `removeEmpty`
* strips only null/undefined (not `false`/`0`), so the boolean toggles and a
* `seed: 0` survive while an omitted `seed` is dropped.
*/
export function toPixal3dPolyGenInput(
data: Pixal3dGenerationSchema
): Trellis2ImageTo3dComfyPolyGenInput {
return removeEmpty({
engine: 'comfy' as const,
model: 'trellis2' as const,
operation: 'imageTo3D' as const,
modelVersion: 'pixal3D' as const,
imageUrl: data.sourceImage.url,
shouldTexture: data.shouldTexture,
shouldRemesh: data.shouldRemesh,
enablePbr: data.enablePbr,
seed: data.seed,
}) as Trellis2ImageTo3dComfyPolyGenInput;
}
@@ -0,0 +1,67 @@
/**
* Trellis.2 Schema (Trellis.2 via Comfy)
*
* Zod schema for the Trellis.2 3D-model generation form. Trellis.2 is the
* base `modelVersion` of the same comfy pipeline Pixal3D rides
* (`Trellis2ImageTo3dComfyPolyGenInput`), differing only in
* `modelVersion: 'trellis2'` vs Pixal3D's `'pixal3D'`. Like Pixal3D / Tripo /
* Hunyuan3D it is image-to-3D only, so there is no text-to-3D branch and no
* `process` discriminator.
*
* Mirrors `src/server/orchestrator/pixal3d/pixal3d.schema.ts`:
* - `sourceImageSchema` reused for the "URL or upload" pattern.
* - `toTrellis2PolyGenInput` converts validated form data to the
* `Trellis2ImageTo3dComfyPolyGenInput` shape (`engine: 'comfy',
* model: 'trellis2', operation: 'imageTo3D', modelVersion: 'trellis2'`)
* consumed by the graph handler when building the `polyGen` step.
*
* Only a small, user-meaningful subset of the client's comfy knobs is surfaced
* (texture / remesh / pbr toggles + seed); the orchestrator applies its own
* defaults for the remaining sampler / step / resolution fields.
*/
import type { Trellis2ImageTo3dComfyPolyGenInput } from '@civitai/client';
import * as z from 'zod';
import { sourceImageSchema } from '~/server/orchestrator/infrastructure/base.schema';
import { removeEmpty } from '~/utils/object-helpers';
// =============================================================================
// Schema
// =============================================================================
export const trellis2GenerationSchema = z.object({
sourceImage: sourceImageSchema,
shouldTexture: z.boolean().default(true),
shouldRemesh: z.boolean().default(true),
enablePbr: z.boolean().default(false),
// Comfy accepts a 32-bit signed int seed; optional so the handler can
// randomize when omitted.
seed: z.number().int().min(-2147483648).max(2147483647).optional(),
});
export type Trellis2GenerationSchema = z.infer<typeof trellis2GenerationSchema>;
// =============================================================================
// Helper — convert validated schema to Trellis.2/Comfy PolyGen input shape
// =============================================================================
/**
* Convert form data to a Trellis.2/Comfy PolyGen input. Used by the graph
* handler when building the `PolyGenStepTemplate` for `submitWorkflow`.
* `removeEmpty` strips only null/undefined (not `false`/`0`), so the boolean
* toggles and a `seed: 0` survive while an omitted `seed` is dropped.
*/
export function toTrellis2PolyGenInput(
data: Trellis2GenerationSchema
): Trellis2ImageTo3dComfyPolyGenInput {
return removeEmpty({
engine: 'comfy' as const,
model: 'trellis2' as const,
operation: 'imageTo3D' as const,
modelVersion: 'trellis2' as const,
imageUrl: data.sourceImage.url,
shouldTexture: data.shouldTexture,
shouldRemesh: data.shouldRemesh,
enablePbr: data.enablePbr,
seed: data.seed,
}) as Trellis2ImageTo3dComfyPolyGenInput;
}
@@ -479,6 +479,8 @@ const featureFlags = createFeatureFlags({
// `polygenVersion: 'v7'` fail the node's schema (see polygen-graph.ts).
tripoGenerator: { availability: ['mod'], fliptKey: 'tripo-generator' },
hunyuan3dGenerator: { availability: ['public'], fliptKey: 'hunyuan3d-generator' },
pixal3dGenerator: { availability: ['mod'], fliptKey: 'pixal3d-generator' },
trellis2Generator: { availability: ['mod'], fliptKey: 'trellis2-generator' },
meshyV7Generator: { availability: ['mod'], fliptKey: 'meshy-v7-generator' },
// Grok Imagine Image 2.0 — gates ONLY the v2.0 entry in the Grok version
// picker; v1.0 / v1.5 stay live regardless, so Grok image + video generation
@@ -102,7 +102,8 @@ vi.mock('@civitai/client', () => {
SdCppSchedule SdxlAiToolkitTrainingInput SeedanceVideoGenInput SeedreamImageGenInput
SeedreamVersion Sora2ImageToVideoInput Sora2TextToVideoInput SubmitWorkflowData
TextToImageStep TextToImageStepTemplate TrainingOutput TrainingStep TrainingStepTemplate
TransactionInfo TransactionType TripoFalPolyGenInput UpdateWorkflowRequest
TransactionInfo TransactionType Trellis2ImageTo3dComfyPolyGenInput
TripoFalPolyGenInput UpdateWorkflowRequest
Veo3VideoGenInput VideoBlob VideoEnhancementInput VideoEnhancementStepTemplate
VideoGenStepTemplate VideoInterpolationStepTemplate VideoUpscalerStepTemplate
ViduQ3VideoGenInput ViduVideoGenInput Wan21CivitaiVideoGenInput
@@ -61,6 +61,8 @@ import { createAceAudioInput } from './ace-audio.handler';
import { createPolyGenInput } from './polygen-graph.handler';
import { createTripoInput } from './tripo-graph.handler';
import { createHunyuan3dInput } from './hunyuan3d-graph.handler';
import { createPixal3dInput } from './pixal3d-graph.handler';
import { createTrellis2Input } from './trellis2-graph.handler';
// Video ecosystem handlers
import { createWanSteps } from './wan.handler';
@@ -261,6 +263,8 @@ export { createAceAudioInput } from './ace-audio.handler';
export { createPolyGenInput } from './polygen-graph.handler';
export { createTripoInput } from './tripo-graph.handler';
export { createHunyuan3dInput } from './hunyuan3d-graph.handler';
export { createPixal3dInput } from './pixal3d-graph.handler';
export { createTrellis2Input } from './trellis2-graph.handler';
// Video ecosystems
export { createWanSteps } from './wan.handler';
@@ -544,6 +548,12 @@ async function createEcosystemStep(
case 'Hunyuan3D':
return createHunyuan3dInput(normalizedData, handlerCtx);
case 'Pixal3D':
return createPixal3dInput(normalizedData, handlerCtx);
case 'Trellis2':
return createTrellis2Input(normalizedData, handlerCtx);
default:
throw new Error(`Unknown ecosystem: ${ecosystem}`);
}
@@ -0,0 +1,47 @@
/**
* Pixal3D Ecosystem Handler (V2 unified pipeline)
*
* Converts a validated pixal3d-graph snapshot into a `PolyGenStepTemplate` so
* the unified generate/whatif pipeline can submit it like any other ecosystem
* step. Mirrors `hunyuan3d-graph.handler.ts` (the other comfy 3D ecosystem);
* differs in the model/modelVersion (`trellis2` / `pixal3D`) and the input
* builder (`toPixal3dPolyGenInput`).
*
* The pixal3d graph carries the standard `images` array (polygen img2model3d
* convention), which we map to the schema's `sourceImage` before handing off to
* the converter. All other node names match the schema 1:1.
*/
import type { PolyGenStepTemplate, Trellis2ImageTo3dComfyPolyGenInput } from '@civitai/client';
import { defineHandler } from './handler-factory';
import type { StepInput } from '.';
import type { GenerationGraphTypes } from '~/shared/data-graph/generation/generation-graph';
import {
toPixal3dPolyGenInput,
type Pixal3dGenerationSchema,
} from '~/server/orchestrator/pixal3d/pixal3d.schema';
import { buildModel3DPreviewStep } from './model3d-preview';
type EcosystemGraphOutput = Extract<GenerationGraphTypes['Ctx'], { ecosystem: string }>;
type Pixal3dCtx = EcosystemGraphOutput & { ecosystem: 'Pixal3D' };
export const createPixal3dInput = defineHandler<Pixal3dCtx, StepInput[]>((data, ctx) => {
const sourceImage = (data as { images?: Array<{ url: string; width: number; height: number }> })
.images?.[0];
const input = toPixal3dPolyGenInput({
...data,
...(sourceImage ? { sourceImage } : {}),
} as unknown as Pixal3dGenerationSchema) as Trellis2ImageTo3dComfyPolyGenInput;
const polyGenStep: PolyGenStepTemplate = {
$type: 'polyGen',
input,
};
const previewStep = buildModel3DPreviewStep(ctx.baseStepIndex);
// Cast to StepInput[] — the shared `StepInput` union lists neither
// `PolyGenStepTemplate` nor the (client-untyped) `model3DPreview` step,
// but the orchestrator queue accepts both natively.
return [polyGenStep, previewStep] as unknown as StepInput[];
});
@@ -0,0 +1,46 @@
/**
* Trellis.2 Ecosystem Handler (V2 unified pipeline)
*
* Converts a validated trellis2-graph snapshot into a `PolyGenStepTemplate` so
* the unified generate/whatif pipeline can submit it like any other ecosystem
* step. Mirrors `pixal3d-graph.handler.ts` (the sibling Trellis2 modelVersion);
* differs only in `modelVersion` (`trellis2`) via `toTrellis2PolyGenInput`.
*
* The trellis2 graph carries the standard `images` array (polygen img2model3d
* convention), which we map to the schema's `sourceImage` before handing off to
* the converter. All other node names match the schema 1:1.
*/
import type { PolyGenStepTemplate, Trellis2ImageTo3dComfyPolyGenInput } from '@civitai/client';
import { defineHandler } from './handler-factory';
import type { StepInput } from '.';
import type { GenerationGraphTypes } from '~/shared/data-graph/generation/generation-graph';
import {
toTrellis2PolyGenInput,
type Trellis2GenerationSchema,
} from '~/server/orchestrator/trellis2/trellis2.schema';
import { buildModel3DPreviewStep } from './model3d-preview';
type EcosystemGraphOutput = Extract<GenerationGraphTypes['Ctx'], { ecosystem: string }>;
type Trellis2Ctx = EcosystemGraphOutput & { ecosystem: 'Trellis2' };
export const createTrellis2Input = defineHandler<Trellis2Ctx, StepInput[]>((data, ctx) => {
const sourceImage = (data as { images?: Array<{ url: string; width: number; height: number }> })
.images?.[0];
const input = toTrellis2PolyGenInput({
...data,
...(sourceImage ? { sourceImage } : {}),
} as unknown as Trellis2GenerationSchema) as Trellis2ImageTo3dComfyPolyGenInput;
const polyGenStep: PolyGenStepTemplate = {
$type: 'polyGen',
input,
};
const previewStep = buildModel3DPreviewStep(ctx.baseStepIndex);
// Cast to StepInput[] — the shared `StepInput` union lists neither
// `PolyGenStepTemplate` nor the (client-untyped) `model3DPreview` step,
// but the orchestrator queue accepts both natively.
return [polyGenStep, previewStep] as unknown as StepInput[];
});
@@ -423,13 +423,14 @@ export const workflowConfigs: WorkflowConfigs = {
// Image segment, where txt2img/img2img both title as "Create Image").
label: 'Create 3D Model',
modeLabel: 'Image to 3D',
description: 'Generate a 3D model from a source image (Meshy, Tripo, or Hunyuan3D)',
description:
'Generate a 3D model from a source image (Meshy, Tripo, Hunyuan3D, Pixal3D, or Trellis.2)',
category: 'model3d',
// Image-to-3D is supported by all three 3D ecosystems; the user picks one
// Image-to-3D is supported by all the 3D ecosystems; the user picks one
// via the `BaseModelInput` "Eco" picker. Text-to-3D (txt2model3d) stays
// PolyGen-only — Tripo/Hunyuan3D have no text-to-3D operation, and neither
// does Meshy v7 (a version inside PolyGen, see polygen-graph.ts).
ecosystemIds: [ECO.PolyGen, ECO.Tripo, ECO.Hunyuan3D],
// PolyGen-only — Tripo/Hunyuan3D/Pixal3D/Trellis.2 have no text-to-3D
// operation, and neither does Meshy v7 (a version inside PolyGen).
ecosystemIds: [ECO.PolyGen, ECO.Tripo, ECO.Hunyuan3D, ECO.Pixal3D, ECO.Trellis2],
featureFlag: 'model3dGenerator',
isNew: true,
},
@@ -81,6 +81,8 @@ import { aceAudioGraph } from './ace-audio-graph';
import { polyGenGraph } from './polygen-graph';
import { tripoGraph } from './tripo-graph';
import { hunyuan3dGraph } from './hunyuan3d-graph';
import { pixal3dGraph } from './pixal3d-graph';
import { trellis2Graph } from './trellis2-graph';
// =============================================================================
// Helper Functions
@@ -143,6 +145,8 @@ type EcosystemGateExt = Pick<
const FEATURE_FLAG_GATED_ECOSYSTEMS: Array<{ key: string; flag: keyof FeatureAccess }> = [
{ key: 'Tripo', flag: 'tripoGenerator' },
{ key: 'Hunyuan3D', flag: 'hunyuan3dGenerator' },
{ key: 'Pixal3D', flag: 'pixal3dGenerator' },
{ key: 'Trellis2', flag: 'trellis2Generator' },
];
/**
@@ -437,6 +441,8 @@ export const ecosystemGraph = new DataGraph<
// the active ecosystem isn't the matching one (same pattern as PolyGen).
{ values: ['Tripo'] as const, graph: tripoGraph },
{ values: ['Hunyuan3D'] as const, graph: hunyuan3dGraph },
{ values: ['Pixal3D'] as const, graph: pixal3dGraph },
{ values: ['Trellis2'] as const, graph: trellis2Graph },
])
// Enhanced compatibility mode - txt2img only, supported ecosystems, hidden for Flux Ultra
.node(
@@ -0,0 +1,58 @@
/**
* Pixal3D Graph
*
* Controls for the Pixal3D (via Comfy / Trellis2) 3D-model generation
* ecosystem. Pixal3D is image-to-3D only, so like Tripo and Hunyuan3D and
* unlike PolyGen (Meshy) there is no `process` discriminator. It rides the
* shared `img2model3d` workflow; the active ecosystem is chosen via the
* `BaseModelInput` picker.
*
* The handler (`pixal3d-graph.handler.ts`) consumes the validated snapshot and
* emits a `PolyGenStepTemplate` matching `pixal3d.schema.ts`. Every node name
* matches a shared 3D Controller in `GenerationForm.tsx` (`images`,
* `shouldTexture`, `shouldRemesh`, `enablePbr`, `seed`), so no Pixal3D-specific
* form block is needed those Controllers auto-show when the active ecosystem
* is Pixal3D (same reuse pattern Hunyuan3D uses for its toggles).
*/
import z from 'zod';
import { DataGraph } from '~/libs/data-graph/data-graph';
import type { GenerationCtx } from './context';
import { imagesNode, seedNode } from './common';
// =============================================================================
// Pixal3D Graph
// =============================================================================
type Pixal3dCtx = { ecosystem: string; workflow: string };
export const pixal3dGraph = new DataGraph<Pixal3dCtx, GenerationCtx>()
// Image-to-3D source — the standard `images` node (min/max 1), matching the
// polygen graph's img2model3d convention. The handler reads `images[0]`.
.node(
'images',
imagesNode({
min: 1,
max: 1,
label: 'Starting image',
description: 'The reference Pixal3D will use to build the 3D mesh',
})
)
.node('shouldTexture', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: true,
})
.node('shouldRemesh', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: true,
})
.node('enablePbr', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: false,
})
.node('seed', seedNode());
export type Pixal3dGraphCtx = ReturnType<typeof pixal3dGraph.init>;
@@ -0,0 +1,58 @@
/**
* Trellis.2 Graph
*
* Controls for the Trellis.2 (via Comfy) 3D-model generation ecosystem.
* Trellis.2 is the base `modelVersion` of the same pipeline Pixal3D rides;
* like Pixal3D / Tripo / Hunyuan3D it is image-to-3D only, so there is no
* `process` discriminator. It rides the shared `img2model3d` workflow; the
* active ecosystem is chosen via the `BaseModelInput` picker.
*
* The handler (`trellis2-graph.handler.ts`) consumes the validated snapshot and
* emits a `PolyGenStepTemplate` matching `trellis2.schema.ts`. Every node name
* matches a shared 3D Controller in `GenerationForm.tsx` (`images`,
* `shouldTexture`, `shouldRemesh`, `enablePbr`, `seed`), so no Trellis.2-specific
* form block is needed those Controllers auto-show when the active ecosystem
* is Trellis2 (same reuse pattern Pixal3D/Hunyuan3D use for their toggles).
*/
import z from 'zod';
import { DataGraph } from '~/libs/data-graph/data-graph';
import type { GenerationCtx } from './context';
import { imagesNode, seedNode } from './common';
// =============================================================================
// Trellis.2 Graph
// =============================================================================
type Trellis2Ctx = { ecosystem: string; workflow: string };
export const trellis2Graph = new DataGraph<Trellis2Ctx, GenerationCtx>()
// Image-to-3D source — the standard `images` node (min/max 1), matching the
// polygen graph's img2model3d convention. The handler reads `images[0]`.
.node(
'images',
imagesNode({
min: 1,
max: 1,
label: 'Starting image',
description: 'The reference Trellis.2 will use to build the 3D mesh',
})
)
.node('shouldTexture', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: true,
})
.node('shouldRemesh', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: true,
})
.node('enablePbr', {
input: z.boolean().optional(),
output: z.boolean(),
defaultValue: false,
})
.node('seed', seedNode());
export type Trellis2GraphCtx = ReturnType<typeof trellis2Graph.init>;