@@ -279,7 +279,27 @@ GroupBox {
279279 // the texture to certain materials isn't working yet).
280280 RowLayout {
281281 Layout .fillWidth : true
282- Item { Layout .fillWidth : true }
282+ // #404: synthesize normal/roughness/height from the current diffuse.
283+ // Only shown on an ONNX build; disabled until a real texture is set.
284+ ThemedButton {
285+ id: pbrSynthBtn
286+ text: " Generate PBR maps from diffuse"
287+ visible: MaterialEditorQML .aiPbrAvailable ()
288+ enabled: MaterialEditorQML .textureName !== " "
289+ && MaterialEditorQML .textureName !== " *Select a texture*"
290+ onClicked: {
291+ pbrStatus .text = " Synthesizing PBR maps…"
292+ MaterialEditorQML .generatePbrFromDiffuse ()
293+ }
294+ }
295+ ThemedLabel {
296+ id: pbrStatus
297+ Layout .fillWidth : true
298+ visible: MaterialEditorQML .aiPbrAvailable ()
299+ text: " "
300+ elide: Text .ElideRight
301+ }
302+ Item { Layout .fillWidth : true ; visible: ! MaterialEditorQML .aiPbrAvailable () }
283303 ThemedButton {
284304 text: " Save Texture As…"
285305 // Bind to textureName (a NOTIFY property) so the
@@ -295,6 +315,165 @@ GroupBox {
295315 MaterialEditorQML .exportCurrentTexture (dest)
296316 }
297317 }
318+
319+ Connections {
320+ target: MaterialEditorQML
321+ function onPbrSynthCompleted (result ) {
322+ pbrStatus .text = result .fromCache ? " PBR maps ready (cached)."
323+ : " PBR maps generated."
324+ }
325+ function onPbrSynthError (err ) { pbrStatus .text = " PBR: " + err }
326+ }
327+ }
328+
329+ // ── PBR slots (multi-slot) view ─────────────────────────────────────
330+ // Expandable section listing every texture unit of the current pass
331+ // (albedo/normal_map/roughness/…), each with its own preview + picker.
332+ // Offered only for PBR-named materials; collapsed by default so the
333+ // single-texture controls above stay the simple path.
334+ GroupBox {
335+ id: pbrSlotsGroup
336+ Layout .fillWidth : true
337+ // Visibility tracks `units` (updated by the Connections block below)
338+ // rather than the Q_INVOKABLE isPbrMaterial() — the latter has no
339+ // NOTIFY signal so a method-call binding would go stale when slots
340+ // are added/removed at runtime (e.g. after Generate PBR).
341+ property var pbrSlotNames: [" albedo" , " diffuse_map" , " normal_map" ,
342+ " roughness" , " metallic" , " ao" , " emissive" ]
343+ visible: units .some (function (u ) { return pbrSlotNames .indexOf (u) >= 0 })
344+ topPadding: 22
345+ leftPadding: 6
346+ rightPadding: 6
347+ bottomPadding: 6
348+
349+ property bool expanded: false
350+ property var units: MaterialEditorQML .textureUnitList
351+
352+ title: " "
353+ background: Rectangle {
354+ color: " transparent"
355+ Rectangle {
356+ anchors .top : parent .top ; anchors .left : parent .left
357+ anchors .right : parent .right ; height: 1
358+ color: MaterialEditorQML .borderColor
359+ }
360+ }
361+ label: Item {
362+ width: pbrSlotsGroup .width ; height: 22
363+ ThemedLabel {
364+ id: pbrSlotsHeader
365+ anchors .left : parent .left ; topPadding: 4
366+ text: (pbrSlotsGroup .expanded ? " ▼ " : " ▶ " )
367+ + " PBR Texture Slots (" + pbrSlotsGroup .units .length + " )"
368+ font .bold : true
369+ }
370+ MouseArea {
371+ anchors .fill : parent
372+ cursorShape: Qt .PointingHandCursor
373+ onClicked: pbrSlotsGroup .expanded = ! pbrSlotsGroup .expanded
374+ }
375+ }
376+
377+ // Refresh the slot list when bindings/selection change.
378+ Connections {
379+ target: MaterialEditorQML
380+ function onTextureUnitsChanged () {
381+ pbrSlotsGroup .units = MaterialEditorQML .textureUnitList
382+ slotRepeater .bump ++
383+ }
384+ function onTextureUnitListChanged () {
385+ pbrSlotsGroup .units = MaterialEditorQML .textureUnitList
386+ slotRepeater .bump ++
387+ }
388+ }
389+
390+ ColumnLayout {
391+ anchors .fill : parent
392+ spacing: 6
393+ visible: pbrSlotsGroup .expanded
394+
395+ Repeater {
396+ id: slotRepeater
397+ model: pbrSlotsGroup .units
398+ property int bump: 0 // force preview re-eval on rebind
399+
400+ RowLayout {
401+ Layout .fillWidth : true
402+ spacing: 8
403+ property int slotIndex: index
404+
405+ // Slot preview thumbnail
406+ Rectangle {
407+ width: 48 ; height: 48
408+ color: MaterialEditorQML .panelColor
409+ border .color : MaterialEditorQML .borderColor
410+ border .width : 1
411+ Image {
412+ anchors .fill : parent; anchors .margins : 2
413+ fillMode: Image .PreserveAspectFit
414+ cache: false
415+ source: {
416+ slotRepeater .bump ; // dependency
417+ var p = MaterialEditorQML .texturePreviewPathForUnit (index)
418+ return p !== " " ? p + " ?v=" + slotRepeater .bump : " "
419+ }
420+ }
421+ }
422+
423+ ColumnLayout {
424+ Layout .fillWidth : true
425+ spacing: 2
426+ ThemedLabel {
427+ text: modelData // the slot/unit name
428+ font .bold : true
429+ font .pixelSize : 11
430+ }
431+ ThemedLabel {
432+ Layout .fillWidth : true
433+ elide: Text .ElideMiddle
434+ font .pixelSize : 10
435+ color: MaterialEditorQML .disabledTextColor
436+ text: {
437+ slotRepeater .bump ;
438+ var t = MaterialEditorQML .textureNameForUnit (index)
439+ return t !== " " ? t : " (empty)"
440+ }
441+ }
442+ }
443+
444+ // Pick an already-loaded texture for this slot.
445+ ThemedComboBox {
446+ Layout .preferredWidth : 150
447+ property var opts: [" — pick texture —" ].concat (MaterialEditorQML .getAvailableTextures ())
448+ model: opts
449+ currentIndex: 0
450+ onActivated : function (i ) {
451+ if (i > 0 ) {
452+ MaterialEditorQML .setTextureForUnit (slotIndex, opts[i])
453+ currentIndex = 0
454+ }
455+ }
456+ }
457+
458+ // Browse for a file to bind into this slot.
459+ ThemedButton {
460+ text: " Load file…"
461+ onClicked: {
462+ var p = MaterialEditorQML .openFileDialog ()
463+ if (p && p .length > 0 )
464+ MaterialEditorQML .loadTextureFileForUnit (slotIndex, p)
465+ }
466+ }
467+ }
468+ }
469+
470+ ThemedLabel {
471+ visible: pbrSlotsGroup .units .length === 0
472+ text: " This material has no texture units."
473+ font .pixelSize : 10
474+ color: MaterialEditorQML .disabledTextColor
475+ }
476+ }
298477 }
299478
300479 // Texture Coordinates Group
0 commit comments