diff --git a/backend/alembic/versions/cfcc7ad1e7d5_add_material_override_to_output_types.py b/backend/alembic/versions/cfcc7ad1e7d5_add_material_override_to_output_types.py new file mode 100644 index 0000000..063dfef --- /dev/null +++ b/backend/alembic/versions/cfcc7ad1e7d5_add_material_override_to_output_types.py @@ -0,0 +1,30 @@ +"""add material_override to output_types + +Revision ID: cfcc7ad1e7d5 +Revises: 4c15abf3cf40 +Create Date: 2026-03-14 12:12:03.706587 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'cfcc7ad1e7d5' +down_revision: Union[str, None] = '4c15abf3cf40' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('output_types', sa.Column('material_override', sa.String(length=200), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('output_types', 'material_override') + # ### end Alembic commands ### diff --git a/backend/app/domains/pipeline/tasks/render_order_line.py b/backend/app/domains/pipeline/tasks/render_order_line.py index f24e907..637d332 100644 --- a/backend/app/domains/pipeline/tasks/render_order_line.py +++ b/backend/app/domains/pipeline/tasks/render_order_line.py @@ -198,6 +198,12 @@ def render_order_line_task(self, order_line_id: str): from app.services.material_service import resolve_material_map material_map = resolve_material_map(material_map) + # Apply global material override from OutputType (e.g. x-ray mode) + if line.output_type and line.output_type.material_override: + override_mat = line.output_type.material_override + material_map = {k: override_mat for k in material_map} + emit(order_line_id, f"Material override active: all parts → {override_mat}") + if template: emit(order_line_id, f"Using render template: {template.name} (collection={template.target_collection}, material_replace={template.material_replace_enabled}, lighting_only={template.lighting_only})") logger.info(f"Render template resolved: '{template.name}' path={template.blend_file_path}, lighting_only={template.lighting_only}") diff --git a/backend/app/domains/rendering/models.py b/backend/app/domains/rendering/models.py index 87f782b..155f926 100644 --- a/backend/app/domains/rendering/models.py +++ b/backend/app/domains/rendering/models.py @@ -44,6 +44,8 @@ class OutputType(Base): DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) + material_override: Mapped[str | None] = mapped_column(String(200), nullable=True, default=None) + workflow_definition_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("workflow_definitions.id", ondelete="SET NULL"), nullable=True ) diff --git a/backend/app/domains/rendering/schemas.py b/backend/app/domains/rendering/schemas.py index a2a57aa..5f9e98d 100644 --- a/backend/app/domains/rendering/schemas.py +++ b/backend/app/domains/rendering/schemas.py @@ -17,6 +17,7 @@ class OutputTypeCreate(BaseModel): transparent_bg: bool = False pricing_tier_id: int | None = None cycles_device: str | None = None + material_override: str | None = None class OutputTypePatch(BaseModel): @@ -34,6 +35,7 @@ class OutputTypePatch(BaseModel): pricing_tier_id: int | None = None cycles_device: str | None = None workflow_definition_id: uuid.UUID | None = None + material_override: str | None = None class OutputTypeOut(BaseModel): @@ -54,6 +56,7 @@ class OutputTypeOut(BaseModel): price_per_item: float | None = None workflow_definition_id: uuid.UUID | None = None workflow_name: str | None = None + material_override: str | None = None is_active: bool created_at: datetime updated_at: datetime diff --git a/frontend/src/api/outputTypes.ts b/frontend/src/api/outputTypes.ts index d7d07b7..4aeba98 100644 --- a/frontend/src/api/outputTypes.ts +++ b/frontend/src/api/outputTypes.ts @@ -17,6 +17,7 @@ export interface OutputType { pricing_tier_name: string | null price_per_item: number | null workflow_definition_id: string | null + material_override: string | null is_active: boolean created_at: string updated_at: string diff --git a/frontend/src/components/admin/OutputTypeTable.tsx b/frontend/src/components/admin/OutputTypeTable.tsx index dfbee75..f1066a6 100644 --- a/frontend/src/components/admin/OutputTypeTable.tsx +++ b/frontend/src/components/admin/OutputTypeTable.tsx @@ -6,6 +6,8 @@ import { listOutputTypes, createOutputType, updateOutputType, deleteOutputType, } from '../../api/outputTypes' import type { OutputType } from '../../api/outputTypes' +import { listMaterials } from '../../api/materials' +import type { Material } from '../../api/materials' import { listPricingTiers } from '../../api/pricing' import type { PricingTier } from '../../api/pricing' import { getWorkflows } from '../../api/workflows' @@ -22,7 +24,7 @@ const ALL_CATEGORIES = [ { key: 'Linear_schiene', label: 'Linear' }, { key: 'Anschlagplatten', label: 'Anschlag' }, ] -const EMPTY_FORM ={ name: '', description: '', renderer: 'threejs', output_format: 'png', sort_order: 0, compatible_categories: [] as string[], render_backend: 'auto', is_animation: false, transparent_bg: false, cycles_device: '' as string, pricing_tier_id: null as number | null, width: '', height: '', engine: '', samples: '', frame_count: '', fps: '', turntable_axis: 'world_z', bg_color: '', noise_threshold: '', denoiser: '', denoising_input_passes: '', denoising_prefilter: '', denoising_quality: '', denoising_use_gpu: '' } +const EMPTY_FORM ={ name: '', description: '', renderer: 'threejs', output_format: 'png', sort_order: 0, compatible_categories: [] as string[], render_backend: 'auto', is_animation: false, transparent_bg: false, cycles_device: '' as string, pricing_tier_id: null as number | null, material_override: '' as string, width: '', height: '', engine: '', samples: '', frame_count: '', fps: '', turntable_axis: 'world_z', bg_color: '', noise_threshold: '', denoiser: '', denoising_input_passes: '', denoising_prefilter: '', denoising_quality: '', denoising_use_gpu: '' } export default function OutputTypeTable() { const qc = useQueryClient() @@ -41,6 +43,12 @@ export default function OutputTypeTable() { queryFn: listPricingTiers, }) + const { data: allMaterials } = useQuery({ + queryKey: ['materials'], + queryFn: listMaterials, + }) + const libraryMaterials = (allMaterials ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name)) + const { data: workflows } = useQuery({ queryKey: ['workflows'], queryFn: getWorkflows, @@ -88,8 +96,9 @@ export default function OutputTypeTable() { transparent_bg: form.transparent_bg, cycles_device: form.cycles_device || null, pricing_tier_id: form.pricing_tier_id, + material_override: form.material_override || null, render_settings: Object.keys(rs).length > 0 ? rs : {}, - }) + } as Partial) }, onSuccess: () => { toast.success('Output type created') @@ -229,6 +238,7 @@ export default function OutputTypeTable() { Resolution Pricing Workflow + Mat Override Sort Active Actions @@ -532,6 +542,18 @@ export default function OutputTypeTable() { ))} + + + + + {ot.material_override ? ( + + {ot.material_override.replace('SCHAEFFLER_', '').replace(/_/g, ' ')} + + ) : ( + + )} + {ot.sort_order} — + + +