prithivMLmods commited on
Commit
0899752
·
verified ·
1 Parent(s): 60b7d8a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +437 -115
app.py CHANGED
@@ -13,7 +13,6 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
13
  from gradio.themes import Soft
14
  from gradio.themes.utils import colors, fonts, sizes
15
 
16
- # --- Theme Configuration ---
17
  colors.orange_red = colors.Color(
18
  name="orange_red",
19
  c50="#FFF0E5",
@@ -82,7 +81,6 @@ class OrangeRedTheme(Soft):
82
 
83
  orange_red_theme = OrangeRedTheme()
84
 
85
- # --- Global Constants & Model Setup ---
86
  MAX_SEED = np.iinfo(np.int32).max
87
 
88
  dtype = torch.bfloat16
@@ -128,8 +126,6 @@ ELEVATION_MAP = {
128
  90: "Above"
129
  }
130
 
131
- # --- Helper Functions ---
132
-
133
  def snap_to_nearest(value, options):
134
  """Snap a value to the nearest option in a list."""
135
  return min(options, key=lambda x: abs(x - value))
@@ -137,6 +133,13 @@ def snap_to_nearest(value, options):
137
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
138
  """
139
  Build a lighting prompt from azimuth and elevation values.
 
 
 
 
 
 
 
140
  """
141
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
142
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
@@ -158,6 +161,9 @@ def infer_lighting_edit(
158
  height: int = 1024,
159
  width: int = 1024,
160
  ):
 
 
 
161
  global loaded
162
  progress = gr.Progress(track_tqdm=True)
163
 
@@ -191,6 +197,7 @@ def infer_lighting_edit(
191
  return result, seed, prompt
192
 
193
  def update_dimensions_on_upload(image):
 
194
  if image is None:
195
  return 1024, 1024
196
  original_width, original_height = image.size
@@ -206,19 +213,29 @@ def update_dimensions_on_upload(image):
206
  new_height = (new_height // 8) * 8
207
  return new_width, new_height
208
 
209
- # --- Custom 3D Component ---
210
-
211
  class LightingControl3D(gr.HTML):
212
  """
213
  A 3D lighting control component using Three.js.
 
 
214
  """
215
  def __init__(self, value=None, imageUrl=None, **kwargs):
216
  if value is None:
217
  value = {"azimuth": 0, "elevation": 0}
218
 
219
  html_template = """
220
- <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: #1a1a1a; border-radius: 12px; overflow: hidden;">
221
- <div id="prompt-overlay" style="position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 8px 16px; border-radius: 8px; font-family: monospace; font-size: 12px; color: #00ff88; white-space: nowrap; z-index: 10;"></div>
 
 
 
 
 
 
 
 
 
 
222
  </div>
223
  """
224
 
@@ -227,7 +244,6 @@ class LightingControl3D(gr.HTML):
227
  const wrapper = element.querySelector('#lighting-control-wrapper');
228
  const promptOverlay = element.querySelector('#prompt-overlay');
229
 
230
- // Wait for THREE to load
231
  const initScene = () => {
232
  if (typeof THREE === 'undefined') {
233
  setTimeout(initScene, 100);
@@ -236,7 +252,10 @@ class LightingControl3D(gr.HTML):
236
 
237
  // Scene setup
238
  const scene = new THREE.Scene();
239
- scene.background = new THREE.Color(0x1a1a1a);
 
 
 
240
 
241
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
242
  camera.position.set(4.5, 3, 4.5);
@@ -247,23 +266,53 @@ class LightingControl3D(gr.HTML):
247
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
248
  renderer.shadowMap.enabled = true;
249
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 
250
  wrapper.insertBefore(renderer.domElement, promptOverlay);
251
 
252
- // Lighting (Ambient)
253
- scene.add(new THREE.AmbientLight(0xffffff, 0.1));
254
 
255
  // Ground plane for shadows
256
  const ground = new THREE.Mesh(
257
- new THREE.PlaneGeometry(10, 10),
258
- new THREE.ShadowMaterial({ opacity: 0.3 })
 
 
 
 
259
  );
260
  ground.rotation.x = -Math.PI / 2;
261
  ground.position.y = 0;
262
  ground.receiveShadow = true;
263
  scene.add(ground);
264
 
265
- // Grid
266
- scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  // Constants
269
  const CENTER = new THREE.Vector3(0, 0.75, 0);
@@ -289,49 +338,87 @@ class LightingControl3D(gr.HTML):
289
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
290
  }
291
 
292
- // Create placeholder texture (smiley face)
293
  function createPlaceholderTexture() {
294
  const canvas = document.createElement('canvas');
295
  canvas.width = 256;
296
  canvas.height = 256;
297
  const ctx = canvas.getContext('2d');
298
- ctx.fillStyle = '#3a3a4a';
 
 
 
 
 
299
  ctx.fillRect(0, 0, 256, 256);
300
- ctx.fillStyle = '#ffcc99';
 
 
 
301
  ctx.beginPath();
302
- ctx.arc(128, 128, 80, 0, Math.PI * 2);
303
- ctx.fill();
304
- ctx.fillStyle = '#333';
 
 
305
  ctx.beginPath();
306
- ctx.arc(100, 110, 10, 0, Math.PI * 2);
307
- ctx.arc(156, 110, 10, 0, Math.PI * 2);
 
 
308
  ctx.fill();
309
- ctx.strokeStyle = '#333';
310
- ctx.lineWidth = 3;
311
  ctx.beginPath();
312
- ctx.arc(128, 130, 35, 0.2, Math.PI - 0.2);
313
- ctx.stroke();
 
 
 
 
 
 
 
314
  return new THREE.CanvasTexture(canvas);
315
  }
316
 
317
  // Target image plane
318
  let currentTexture = createPlaceholderTexture();
319
- const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
 
 
 
 
 
320
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
321
  targetPlane.position.copy(CENTER);
322
  targetPlane.receiveShadow = true;
 
323
  scene.add(targetPlane);
324
 
 
 
 
 
 
 
 
325
  // Function to update texture from image URL
326
  function updateTextureFromUrl(url) {
327
  if (!url) {
328
  planeMaterial.map = createPlaceholderTexture();
329
  planeMaterial.needsUpdate = true;
330
  scene.remove(targetPlane);
 
331
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
332
  targetPlane.position.copy(CENTER);
333
  targetPlane.receiveShadow = true;
 
334
  scene.add(targetPlane);
 
 
 
 
335
  return;
336
  }
337
 
@@ -356,13 +443,22 @@ class LightingControl3D(gr.HTML):
356
  planeWidth = maxSize * aspect;
357
  }
358
  scene.remove(targetPlane);
 
359
  targetPlane = new THREE.Mesh(
360
  new THREE.PlaneGeometry(planeWidth, planeHeight),
361
  planeMaterial
362
  );
363
  targetPlane.position.copy(CENTER);
364
  targetPlane.receiveShadow = true;
 
365
  scene.add(targetPlane);
 
 
 
 
 
 
 
366
  }
367
  }, undefined, (err) => {
368
  console.error('Failed to load texture:', err);
@@ -373,41 +469,130 @@ class LightingControl3D(gr.HTML):
373
  updateTextureFromUrl(props.imageUrl);
374
  }
375
 
376
- // --- LIGHT MODEL REDESIGN (Softbox) ---
 
 
377
  const lightGroup = new THREE.Group();
378
 
379
- // Box Geometry for Softbox
380
- const sbGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.3);
381
-
382
- // Materials: Red body, White light face
383
- const sbMatRed = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
384
- const sbMatWhite = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 2 });
385
-
386
- // In Three.js, Face 4 is +Z. When we lookAt(), +Z points to target.
387
- // 0:+x, 1:-x, 2:+y, 3:-y, 4:+z, 5:-z
388
- const sbMaterials = [
389
- sbMatRed, // Right
390
- sbMatRed, // Left
391
- sbMatRed, // Top
392
- sbMatRed, // Bottom
393
- sbMatWhite, // Front (Light Face)
394
- sbMatRed // Back
395
- ];
396
-
397
- const softbox = new THREE.Mesh(sbGeometry, sbMaterials);
398
- lightGroup.add(softbox);
399
-
400
- // Actual SpotLight source
401
- const spotLight = new THREE.SpotLight(0xffffff, 10, 10, Math.PI / 3, 1, 1);
402
- spotLight.position.set(0, 0, 0.1); // Slightly in front of the box center
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  spotLight.castShadow = true;
404
  spotLight.shadow.mapSize.width = 1024;
405
  spotLight.shadow.mapSize.height = 1024;
406
  spotLight.shadow.camera.near = 0.5;
407
- spotLight.shadow.camera.far = 500;
408
- spotLight.shadow.bias = -0.005;
409
  lightGroup.add(spotLight);
410
 
 
 
 
 
 
411
  const lightTarget = new THREE.Object3D();
412
  lightTarget.position.copy(CENTER);
413
  scene.add(lightTarget);
@@ -415,66 +600,168 @@ class LightingControl3D(gr.HTML):
415
 
416
  scene.add(lightGroup);
417
 
418
- // --- HANDLES REDESIGN ---
 
 
 
419
 
420
- // YELLOW: Azimuth ring
421
  const azimuthRing = new THREE.Mesh(
422
- new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
423
- new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
 
 
 
 
 
 
424
  );
425
  azimuthRing.rotation.x = Math.PI / 2;
426
  azimuthRing.position.y = 0.05;
427
  scene.add(azimuthRing);
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  const azimuthHandle = new THREE.Mesh(
430
- new THREE.SphereGeometry(0.18, 16, 16),
431
- new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
 
 
 
 
 
 
432
  );
433
  azimuthHandle.userData.type = 'azimuth';
 
434
  scene.add(azimuthHandle);
435
 
436
- // BLUE: Elevation arc
 
 
 
 
437
  const arcPoints = [];
438
- for (let i = 0; i <= 32; i++) {
439
- const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
440
- arcPoints.push(new THREE.Vector3(-0.8, ELEVATION_RADIUS * Math.sin(angle) + CENTER.y, ELEVATION_RADIUS * Math.cos(angle)));
 
 
 
 
441
  }
442
  const arcCurve = new THREE.CatmullRomCurve3(arcPoints);
443
  const elevationArc = new THREE.Mesh(
444
- new THREE.TubeGeometry(arcCurve, 32, 0.04, 8, false),
445
- new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.3 })
 
 
 
 
 
 
446
  );
447
  scene.add(elevationArc);
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  const elevationHandle = new THREE.Mesh(
450
- new THREE.SphereGeometry(0.18, 16, 16),
451
- new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
 
 
 
 
 
 
452
  );
453
  elevationHandle.userData.type = 'elevation';
 
454
  scene.add(elevationHandle);
455
 
456
- // --- RESET BUTTON REDESIGN ---
 
 
457
  const refreshBtn = document.createElement('button');
458
- refreshBtn.innerHTML = 'Reset View';
459
- refreshBtn.style.position = 'absolute';
460
- refreshBtn.style.top = '15px';
461
- refreshBtn.style.right = '15px';
462
- refreshBtn.style.background = 'rgba(255, 255, 255, 0.9)';
463
- refreshBtn.style.border = 'none';
464
- refreshBtn.style.borderRadius = '20px'; // Pill shape
465
- refreshBtn.style.padding = '8px 16px';
466
- refreshBtn.style.fontFamily = 'sans-serif';
467
- refreshBtn.style.fontWeight = 'bold';
468
- refreshBtn.style.fontSize = '12px';
469
- refreshBtn.style.color = '#333';
470
- refreshBtn.style.cursor = 'pointer';
471
- refreshBtn.style.zIndex = '20';
472
- refreshBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
473
- refreshBtn.style.transition = 'background 0.2s';
474
-
475
- refreshBtn.onmouseover = () => { refreshBtn.style.background = '#ffffff'; };
476
- refreshBtn.onmouseout = () => { refreshBtn.style.background = 'rgba(255, 255, 255, 0.9)'; };
477
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  wrapper.appendChild(refreshBtn);
479
 
480
  refreshBtn.addEventListener('click', () => {
@@ -496,19 +783,31 @@ class LightingControl3D(gr.HTML):
496
  lightGroup.position.set(lightX, lightY, lightZ);
497
  lightGroup.lookAt(CENTER);
498
 
499
- azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
500
- elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
 
 
 
 
 
 
 
 
501
 
502
  // Update prompt
503
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
504
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
505
- let prompt = 'Light source from';
506
  if (elSnap !== 0) {
507
  prompt += ' ' + elevationNames[String(elSnap)];
508
  } else {
509
  prompt += ' the ' + azimuthNames[azSnap];
510
  }
511
  promptOverlay.textContent = prompt;
 
 
 
 
512
  }
513
 
514
  function updatePropsAndTrigger() {
@@ -524,7 +823,6 @@ class LightingControl3D(gr.HTML):
524
  const mouse = new THREE.Vector2();
525
  let isDragging = false;
526
  let dragTarget = null;
527
- let dragStartMouse = new THREE.Vector2();
528
  const intersection = new THREE.Vector3();
529
 
530
  const canvas = renderer.domElement;
@@ -540,9 +838,8 @@ class LightingControl3D(gr.HTML):
540
  if (intersects.length > 0) {
541
  isDragging = true;
542
  dragTarget = intersects[0].object;
543
- dragTarget.material.emissiveIntensity = 1.0;
544
- dragTarget.scale.setScalar(1.3);
545
- dragStartMouse.copy(mouse);
546
  canvas.style.cursor = 'grabbing';
547
  }
548
  });
@@ -562,11 +859,15 @@ class LightingControl3D(gr.HTML):
562
  if (azimuthAngle < 0) azimuthAngle += 360;
563
  }
564
  } else if (dragTarget.userData.type === 'elevation') {
565
- const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -0.8);
566
  if (raycaster.ray.intersectPlane(plane, intersection)) {
567
  const relY = intersection.y - CENTER.y;
568
  const relZ = intersection.z;
569
- elevationAngle = THREE.MathUtils.clamp(THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)), -90, 90);
 
 
 
 
570
  }
571
  }
572
  updatePositions();
@@ -574,12 +875,12 @@ class LightingControl3D(gr.HTML):
574
  raycaster.setFromCamera(mouse, camera);
575
  const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
576
  [azimuthHandle, elevationHandle].forEach(h => {
577
- h.material.emissiveIntensity = 0.5;
578
  h.scale.setScalar(1);
579
  });
580
  if (intersects.length > 0) {
581
- intersects[0].object.material.emissiveIntensity = 0.8;
582
- intersects[0].object.scale.setScalar(1.1);
583
  canvas.style.cursor = 'grab';
584
  } else {
585
  canvas.style.cursor = 'default';
@@ -589,7 +890,7 @@ class LightingControl3D(gr.HTML):
589
 
590
  const onMouseUp = () => {
591
  if (dragTarget) {
592
- dragTarget.material.emissiveIntensity = 0.5;
593
  dragTarget.scale.setScalar(1);
594
 
595
  // Snap and animate
@@ -625,6 +926,7 @@ class LightingControl3D(gr.HTML):
625
 
626
  canvas.addEventListener('mouseup', onMouseUp);
627
  canvas.addEventListener('mouseleave', onMouseUp);
 
628
  // Touch support
629
  canvas.addEventListener('touchstart', (e) => {
630
  e.preventDefault();
@@ -639,9 +941,8 @@ class LightingControl3D(gr.HTML):
639
  if (intersects.length > 0) {
640
  isDragging = true;
641
  dragTarget = intersects[0].object;
642
- dragTarget.material.emissiveIntensity = 1.0;
643
- dragTarget.scale.setScalar(1.3);
644
- dragStartMouse.copy(mouse);
645
  }
646
  }, { passive: false });
647
 
@@ -654,6 +955,7 @@ class LightingControl3D(gr.HTML):
654
 
655
  if (isDragging && dragTarget) {
656
  raycaster.setFromCamera(mouse, camera);
 
657
  if (dragTarget.userData.type === 'azimuth') {
658
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -0.05);
659
  if (raycaster.ray.intersectPlane(plane, intersection)) {
@@ -661,11 +963,15 @@ class LightingControl3D(gr.HTML):
661
  if (azimuthAngle < 0) azimuthAngle += 360;
662
  }
663
  } else if (dragTarget.userData.type === 'elevation') {
664
- const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -0.8);
665
  if (raycaster.ray.intersectPlane(plane, intersection)) {
666
  const relY = intersection.y - CENTER.y;
667
  const relZ = intersection.z;
668
- elevationAngle = THREE.MathUtils.clamp(THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)), -90, 90);
 
 
 
 
669
  }
670
  }
671
  updatePositions();
@@ -676,6 +982,7 @@ class LightingControl3D(gr.HTML):
676
  e.preventDefault();
677
  onMouseUp();
678
  }, { passive: false });
 
679
  canvas.addEventListener('touchcancel', (e) => {
680
  e.preventDefault();
681
  onMouseUp();
@@ -684,9 +991,24 @@ class LightingControl3D(gr.HTML):
684
  // Initial update
685
  updatePositions();
686
 
687
- // Render loop
 
688
  function render() {
689
  requestAnimationFrame(render);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  renderer.render(scene, camera);
691
  }
692
  render();
@@ -698,7 +1020,7 @@ class LightingControl3D(gr.HTML):
698
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
699
  }).observe(wrapper);
700
 
701
- // Store update functions for external calls
702
  wrapper._updateFromProps = (newVal) => {
703
  if (newVal && typeof newVal === 'object') {
704
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
@@ -741,8 +1063,6 @@ class LightingControl3D(gr.HTML):
741
  **kwargs
742
  )
743
 
744
- # --- Gradio UI Layout ---
745
-
746
  css = '''
747
  #col-container { max-width: 1200px; margin: 0 auto; }
748
  .dark .progress-text { color: white !important; }
@@ -750,6 +1070,7 @@ css = '''
750
  .slider-row { display: flex; gap: 10px; align-items: center; }
751
  #main-title h1 {font-size: 2.4em !important;}
752
  '''
 
753
  with gr.Blocks(css=css) as demo:
754
  gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
755
  gr.Markdown("Control lighting directions using the **3D viewport** or **sliders**. Use [Multi-Angle-Lighting](https://huggingface.co/dx8152/Qwen-Edit-2509-Multi-Angle-Lighting) LoRA for precise lighting control, paired with [Rapid-AIO-V19](https://huggingface.co/prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19).")
@@ -770,7 +1091,7 @@ with gr.Blocks(css=css) as demo:
770
  gr.Markdown("### Slider Controls")
771
 
772
  azimuth_slider = gr.Slider(
773
- label="Azimuth (Horizontal Rotation)",
774
  minimum=0,
775
  maximum=315,
776
  step=45,
@@ -779,7 +1100,7 @@ with gr.Blocks(css=css) as demo:
779
  )
780
 
781
  elevation_slider = gr.Slider(
782
- label="Elevation (Vertical Angle)",
783
  minimum=-90,
784
  maximum=90,
785
  step=90,
@@ -828,6 +1149,7 @@ with gr.Blocks(css=css) as demo:
828
  """Update the 3D component with the uploaded image."""
829
  if image is None:
830
  return gr.update(imageUrl=None)
 
831
  import base64
832
  from io import BytesIO
833
  buffered = BytesIO()
 
13
  from gradio.themes import Soft
14
  from gradio.themes.utils import colors, fonts, sizes
15
 
 
16
  colors.orange_red = colors.Color(
17
  name="orange_red",
18
  c50="#FFF0E5",
 
81
 
82
  orange_red_theme = OrangeRedTheme()
83
 
 
84
  MAX_SEED = np.iinfo(np.int32).max
85
 
86
  dtype = torch.bfloat16
 
126
  90: "Above"
127
  }
128
 
 
 
129
  def snap_to_nearest(value, options):
130
  """Snap a value to the nearest option in a list."""
131
  return min(options, key=lambda x: abs(x - value))
 
133
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
134
  """
135
  Build a lighting prompt from azimuth and elevation values.
136
+
137
+ Args:
138
+ azimuth: Horizontal rotation in degrees (0-360)
139
+ elevation: Vertical angle in degrees (-90 to 90)
140
+
141
+ Returns:
142
+ Formatted prompt string for the LoRA
143
  """
144
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
145
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
 
161
  height: int = 1024,
162
  width: int = 1024,
163
  ):
164
+ """
165
+ Edit the lighting of an image using Qwen Image Edit 2511 with multi-angle lighting LoRA.
166
+ """
167
  global loaded
168
  progress = gr.Progress(track_tqdm=True)
169
 
 
197
  return result, seed, prompt
198
 
199
  def update_dimensions_on_upload(image):
200
+ """Compute recommended dimensions preserving aspect ratio."""
201
  if image is None:
202
  return 1024, 1024
203
  original_width, original_height = image.size
 
213
  new_height = (new_height // 8) * 8
214
  return new_width, new_height
215
 
 
 
216
  class LightingControl3D(gr.HTML):
217
  """
218
  A 3D lighting control component using Three.js.
219
+ Outputs: { azimuth: number, elevation: number }
220
+ Accepts imageUrl prop to display user's uploaded image on the plane.
221
  """
222
  def __init__(self, value=None, imageUrl=None, **kwargs):
223
  if value is None:
224
  value = {"azimuth": 0, "elevation": 0}
225
 
226
  html_template = """
227
+ <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: linear-gradient(180deg, #0d1117 0%, #161b22 50%, #21262d 100%); border-radius: 12px; overflow: hidden; border: 1px solid #30363d;">
228
+ <div id="prompt-overlay" style="position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.85); padding: 10px 20px; border-radius: 20px; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; color: #58a6ff; white-space: nowrap; z-index: 10; border: 1px solid #30363d; box-shadow: 0 4px 12px rgba(0,0,0,0.4);"></div>
229
+ <div id="legend-overlay" style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.75); padding: 10px 14px; border-radius: 10px; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 11px; color: #c9d1d9; z-index: 10; border: 1px solid #30363d;">
230
+ <div style="display: flex; align-items: center; margin-bottom: 6px;">
231
+ <div style="width: 12px; height: 12px; background: #ffd700; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 6px #ffd700;"></div>
232
+ <span>Azimuth (Direction)</span>
233
+ </div>
234
+ <div style="display: flex; align-items: center;">
235
+ <div style="width: 12px; height: 12px; background: #0088ff; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 6px #0088ff;"></div>
236
+ <span>Elevation (Height)</span>
237
+ </div>
238
+ </div>
239
  </div>
240
  """
241
 
 
244
  const wrapper = element.querySelector('#lighting-control-wrapper');
245
  const promptOverlay = element.querySelector('#prompt-overlay');
246
 
 
247
  const initScene = () => {
248
  if (typeof THREE === 'undefined') {
249
  setTimeout(initScene, 100);
 
252
 
253
  // Scene setup
254
  const scene = new THREE.Scene();
255
+ scene.background = new THREE.Color(0x0d1117);
256
+
257
+ // Add fog for depth
258
+ scene.fog = new THREE.Fog(0x0d1117, 8, 20);
259
 
260
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
261
  camera.position.set(4.5, 3, 4.5);
 
266
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
267
  renderer.shadowMap.enabled = true;
268
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
269
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
270
+ renderer.toneMappingExposure = 1.2;
271
  wrapper.insertBefore(renderer.domElement, promptOverlay);
272
 
273
+ // Ambient lighting
274
+ scene.add(new THREE.AmbientLight(0x404050, 0.3));
275
 
276
  // Ground plane for shadows
277
  const ground = new THREE.Mesh(
278
+ new THREE.PlaneGeometry(12, 12),
279
+ new THREE.MeshStandardMaterial({
280
+ color: 0x1a1f25,
281
+ roughness: 0.9,
282
+ metalness: 0.1
283
+ })
284
  );
285
  ground.rotation.x = -Math.PI / 2;
286
  ground.position.y = 0;
287
  ground.receiveShadow = true;
288
  scene.add(ground);
289
 
290
+ // Create circular grid pattern
291
+ const gridGroup = new THREE.Group();
292
+
293
+ // Radial lines
294
+ for (let i = 0; i < 16; i++) {
295
+ const angle = (i / 16) * Math.PI * 2;
296
+ const lineGeo = new THREE.BufferGeometry().setFromPoints([
297
+ new THREE.Vector3(0, 0.01, 0),
298
+ new THREE.Vector3(Math.sin(angle) * 4, 0.01, Math.cos(angle) * 4)
299
+ ]);
300
+ const lineMat = new THREE.LineBasicMaterial({ color: 0x2d333b, transparent: true, opacity: 0.5 });
301
+ gridGroup.add(new THREE.Line(lineGeo, lineMat));
302
+ }
303
+
304
+ // Concentric circles
305
+ for (let r = 1; r <= 4; r++) {
306
+ const circlePoints = [];
307
+ for (let i = 0; i <= 64; i++) {
308
+ const angle = (i / 64) * Math.PI * 2;
309
+ circlePoints.push(new THREE.Vector3(Math.sin(angle) * r, 0.01, Math.cos(angle) * r));
310
+ }
311
+ const circleGeo = new THREE.BufferGeometry().setFromPoints(circlePoints);
312
+ const circleMat = new THREE.LineBasicMaterial({ color: 0x2d333b, transparent: true, opacity: 0.4 });
313
+ gridGroup.add(new THREE.Line(circleGeo, circleMat));
314
+ }
315
+ scene.add(gridGroup);
316
 
317
  // Constants
318
  const CENTER = new THREE.Vector3(0, 0.75, 0);
 
338
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
339
  }
340
 
341
+ // Create placeholder texture
342
  function createPlaceholderTexture() {
343
  const canvas = document.createElement('canvas');
344
  canvas.width = 256;
345
  canvas.height = 256;
346
  const ctx = canvas.getContext('2d');
347
+
348
+ // Gradient background
349
+ const gradient = ctx.createLinearGradient(0, 0, 256, 256);
350
+ gradient.addColorStop(0, '#2d333b');
351
+ gradient.addColorStop(1, '#22272e');
352
+ ctx.fillStyle = gradient;
353
  ctx.fillRect(0, 0, 256, 256);
354
+
355
+ // Image icon
356
+ ctx.strokeStyle = '#484f58';
357
+ ctx.lineWidth = 3;
358
  ctx.beginPath();
359
+ ctx.roundRect(60, 70, 136, 116, 8);
360
+ ctx.stroke();
361
+
362
+ // Mountain
363
+ ctx.fillStyle = '#484f58';
364
  ctx.beginPath();
365
+ ctx.moveTo(80, 160);
366
+ ctx.lineTo(128, 110);
367
+ ctx.lineTo(176, 160);
368
+ ctx.closePath();
369
  ctx.fill();
370
+
371
+ // Sun
372
  ctx.beginPath();
373
+ ctx.arc(155, 100, 15, 0, Math.PI * 2);
374
+ ctx.fill();
375
+
376
+ // Text
377
+ ctx.fillStyle = '#6e7681';
378
+ ctx.font = '14px system-ui';
379
+ ctx.textAlign = 'center';
380
+ ctx.fillText('Upload Image', 128, 210);
381
+
382
  return new THREE.CanvasTexture(canvas);
383
  }
384
 
385
  // Target image plane
386
  let currentTexture = createPlaceholderTexture();
387
+ const planeMaterial = new THREE.MeshStandardMaterial({
388
+ map: currentTexture,
389
+ side: THREE.DoubleSide,
390
+ roughness: 0.3,
391
+ metalness: 0.1
392
+ });
393
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
394
  targetPlane.position.copy(CENTER);
395
  targetPlane.receiveShadow = true;
396
+ targetPlane.castShadow = true;
397
  scene.add(targetPlane);
398
 
399
+ // Add subtle frame around image
400
+ const frameGeo = new THREE.BoxGeometry(1.3, 1.3, 0.05);
401
+ const frameMat = new THREE.MeshStandardMaterial({ color: 0x30363d, roughness: 0.5, metalness: 0.3 });
402
+ const frame = new THREE.Mesh(frameGeo, frameMat);
403
+ frame.position.set(CENTER.x, CENTER.y, CENTER.z - 0.03);
404
+ scene.add(frame);
405
+
406
  // Function to update texture from image URL
407
  function updateTextureFromUrl(url) {
408
  if (!url) {
409
  planeMaterial.map = createPlaceholderTexture();
410
  planeMaterial.needsUpdate = true;
411
  scene.remove(targetPlane);
412
+ scene.remove(frame);
413
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
414
  targetPlane.position.copy(CENTER);
415
  targetPlane.receiveShadow = true;
416
+ targetPlane.castShadow = true;
417
  scene.add(targetPlane);
418
+
419
+ const newFrame = new THREE.Mesh(new THREE.BoxGeometry(1.3, 1.3, 0.05), frameMat);
420
+ newFrame.position.set(CENTER.x, CENTER.y, CENTER.z - 0.03);
421
+ scene.add(newFrame);
422
  return;
423
  }
424
 
 
443
  planeWidth = maxSize * aspect;
444
  }
445
  scene.remove(targetPlane);
446
+ scene.remove(frame);
447
  targetPlane = new THREE.Mesh(
448
  new THREE.PlaneGeometry(planeWidth, planeHeight),
449
  planeMaterial
450
  );
451
  targetPlane.position.copy(CENTER);
452
  targetPlane.receiveShadow = true;
453
+ targetPlane.castShadow = true;
454
  scene.add(targetPlane);
455
+
456
+ const newFrame = new THREE.Mesh(
457
+ new THREE.BoxGeometry(planeWidth + 0.1, planeHeight + 0.1, 0.05),
458
+ frameMat
459
+ );
460
+ newFrame.position.set(CENTER.x, CENTER.y, CENTER.z - 0.03);
461
+ scene.add(newFrame);
462
  }
463
  }, undefined, (err) => {
464
  console.error('Failed to load texture:', err);
 
469
  updateTextureFromUrl(props.imageUrl);
470
  }
471
 
472
+ // ============================================
473
+ // REDESIGNED SOFTBOX LIGHT
474
+ // ============================================
475
  const lightGroup = new THREE.Group();
476
 
477
+ // Softbox outer frame (red)
478
+ const softboxFrameMat = new THREE.MeshStandardMaterial({
479
+ color: 0xcc2222,
480
+ roughness: 0.4,
481
+ metalness: 0.6,
482
+ emissive: 0x441111,
483
+ emissiveIntensity: 0.3
484
+ });
485
+
486
+ // Main body - octagonal shape approximated with cylinder
487
+ const softboxBody = new THREE.Mesh(
488
+ new THREE.CylinderGeometry(0.35, 0.45, 0.15, 8),
489
+ softboxFrameMat
490
+ );
491
+ softboxBody.rotation.x = Math.PI / 2;
492
+ softboxBody.position.z = 0.1;
493
+ lightGroup.add(softboxBody);
494
+
495
+ // Back panel (red)
496
+ const backPanel = new THREE.Mesh(
497
+ new THREE.CylinderGeometry(0.35, 0.35, 0.05, 8),
498
+ softboxFrameMat
499
+ );
500
+ backPanel.rotation.x = Math.PI / 2;
501
+ backPanel.position.z = 0.2;
502
+ lightGroup.add(backPanel);
503
+
504
+ // Inner diffuser ring
505
+ const diffuserRing = new THREE.Mesh(
506
+ new THREE.TorusGeometry(0.38, 0.03, 8, 8),
507
+ new THREE.MeshStandardMaterial({
508
+ color: 0x992222,
509
+ roughness: 0.3,
510
+ metalness: 0.7
511
+ })
512
+ );
513
+ diffuserRing.rotation.x = Math.PI / 2;
514
+ diffuserRing.position.z = 0.02;
515
+ lightGroup.add(diffuserRing);
516
+
517
+ // White diffuser panel (the light emitting surface)
518
+ const diffuserMat = new THREE.MeshStandardMaterial({
519
+ color: 0xffffff,
520
+ emissive: 0xffffff,
521
+ emissiveIntensity: 1.5,
522
+ roughness: 0.1,
523
+ metalness: 0,
524
+ transparent: true,
525
+ opacity: 0.95
526
+ });
527
+
528
+ const diffuserPanel = new THREE.Mesh(
529
+ new THREE.CircleGeometry(0.36, 8),
530
+ diffuserMat
531
+ );
532
+ diffuserPanel.position.z = 0.01;
533
+ lightGroup.add(diffuserPanel);
534
+
535
+ // Inner glow effect
536
+ const glowMat = new THREE.MeshBasicMaterial({
537
+ color: 0xffffff,
538
+ transparent: true,
539
+ opacity: 0.3,
540
+ side: THREE.DoubleSide
541
+ });
542
+ const glowPanel = new THREE.Mesh(
543
+ new THREE.CircleGeometry(0.42, 8),
544
+ glowMat
545
+ );
546
+ glowPanel.position.z = -0.01;
547
+ lightGroup.add(glowPanel);
548
+
549
+ // Mounting bracket
550
+ const bracketMat = new THREE.MeshStandardMaterial({
551
+ color: 0x1a1a1a,
552
+ roughness: 0.3,
553
+ metalness: 0.8
554
+ });
555
+
556
+ const bracket = new THREE.Mesh(
557
+ new THREE.BoxGeometry(0.08, 0.08, 0.15),
558
+ bracketMat
559
+ );
560
+ bracket.position.set(0, 0, 0.28);
561
+ lightGroup.add(bracket);
562
+
563
+ // Stand connection
564
+ const standPole = new THREE.Mesh(
565
+ new THREE.CylinderGeometry(0.02, 0.02, 0.3, 8),
566
+ bracketMat
567
+ );
568
+ standPole.rotation.x = Math.PI / 2;
569
+ standPole.position.set(0, 0, 0.42);
570
+ lightGroup.add(standPole);
571
+
572
+ // Small indicator light on bracket
573
+ const indicatorLight = new THREE.Mesh(
574
+ new THREE.SphereGeometry(0.02, 8, 8),
575
+ new THREE.MeshBasicMaterial({ color: 0x00ff00 })
576
+ );
577
+ indicatorLight.position.set(0.05, 0, 0.25);
578
+ lightGroup.add(indicatorLight);
579
+
580
+ // Spotlight for actual lighting
581
+ const spotLight = new THREE.SpotLight(0xffffff, 15, 12, Math.PI / 3, 0.8, 1);
582
+ spotLight.position.set(0, 0, -0.05);
583
  spotLight.castShadow = true;
584
  spotLight.shadow.mapSize.width = 1024;
585
  spotLight.shadow.mapSize.height = 1024;
586
  spotLight.shadow.camera.near = 0.5;
587
+ spotLight.shadow.camera.far = 15;
588
+ spotLight.shadow.bias = -0.002;
589
  lightGroup.add(spotLight);
590
 
591
+ // Point light for additional glow
592
+ const pointLight = new THREE.PointLight(0xffffff, 3, 8);
593
+ pointLight.position.set(0, 0, -0.1);
594
+ lightGroup.add(pointLight);
595
+
596
  const lightTarget = new THREE.Object3D();
597
  lightTarget.position.copy(CENTER);
598
  scene.add(lightTarget);
 
600
 
601
  scene.add(lightGroup);
602
 
603
+ // ============================================
604
+ // YELLOW: Azimuth ring and handle
605
+ // ============================================
606
+ const azimuthColor = 0xffd700; // Gold/Yellow
607
 
 
608
  const azimuthRing = new THREE.Mesh(
609
+ new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.035, 16, 64),
610
+ new THREE.MeshStandardMaterial({
611
+ color: azimuthColor,
612
+ emissive: azimuthColor,
613
+ emissiveIntensity: 0.4,
614
+ roughness: 0.3,
615
+ metalness: 0.7
616
+ })
617
  );
618
  azimuthRing.rotation.x = Math.PI / 2;
619
  azimuthRing.position.y = 0.05;
620
  scene.add(azimuthRing);
621
 
622
+ // Direction markers on azimuth ring
623
+ const directions = ['N', 'E', 'S', 'W'];
624
+ const dirAngles = [0, 90, 180, 270];
625
+ dirAngles.forEach((angle, i) => {
626
+ const rad = THREE.MathUtils.degToRad(angle);
627
+ const marker = new THREE.Mesh(
628
+ new THREE.SphereGeometry(0.06, 8, 8),
629
+ new THREE.MeshStandardMaterial({
630
+ color: 0x888888,
631
+ emissive: 0x444444,
632
+ emissiveIntensity: 0.3
633
+ })
634
+ );
635
+ marker.position.set(
636
+ AZIMUTH_RADIUS * Math.sin(rad),
637
+ 0.05,
638
+ AZIMUTH_RADIUS * Math.cos(rad)
639
+ );
640
+ scene.add(marker);
641
+ });
642
+
643
  const azimuthHandle = new THREE.Mesh(
644
+ new THREE.SphereGeometry(0.18, 32, 32),
645
+ new THREE.MeshStandardMaterial({
646
+ color: azimuthColor,
647
+ emissive: azimuthColor,
648
+ emissiveIntensity: 0.6,
649
+ roughness: 0.2,
650
+ metalness: 0.8
651
+ })
652
  );
653
  azimuthHandle.userData.type = 'azimuth';
654
+ azimuthHandle.castShadow = true;
655
  scene.add(azimuthHandle);
656
 
657
+ // ============================================
658
+ // BLUE: Elevation arc and handle
659
+ // ============================================
660
+ const elevationColor = 0x0088ff; // Blue
661
+
662
  const arcPoints = [];
663
+ for (let i = 0; i <= 48; i++) {
664
+ const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 48));
665
+ arcPoints.push(new THREE.Vector3(
666
+ -0.8,
667
+ ELEVATION_RADIUS * Math.sin(angle) + CENTER.y,
668
+ ELEVATION_RADIUS * Math.cos(angle)
669
+ ));
670
  }
671
  const arcCurve = new THREE.CatmullRomCurve3(arcPoints);
672
  const elevationArc = new THREE.Mesh(
673
+ new THREE.TubeGeometry(arcCurve, 48, 0.035, 8, false),
674
+ new THREE.MeshStandardMaterial({
675
+ color: elevationColor,
676
+ emissive: elevationColor,
677
+ emissiveIntensity: 0.4,
678
+ roughness: 0.3,
679
+ metalness: 0.7
680
+ })
681
  );
682
  scene.add(elevationArc);
683
 
684
+ // Elevation level markers
685
+ [-90, 0, 90].forEach(elev => {
686
+ const rad = THREE.MathUtils.degToRad(elev);
687
+ const marker = new THREE.Mesh(
688
+ new THREE.SphereGeometry(0.05, 8, 8),
689
+ new THREE.MeshStandardMaterial({
690
+ color: 0x666666,
691
+ emissive: 0x333333,
692
+ emissiveIntensity: 0.3
693
+ })
694
+ );
695
+ marker.position.set(
696
+ -0.8,
697
+ ELEVATION_RADIUS * Math.sin(rad) + CENTER.y,
698
+ ELEVATION_RADIUS * Math.cos(rad)
699
+ );
700
+ scene.add(marker);
701
+ });
702
+
703
  const elevationHandle = new THREE.Mesh(
704
+ new THREE.SphereGeometry(0.18, 32, 32),
705
+ new THREE.MeshStandardMaterial({
706
+ color: elevationColor,
707
+ emissive: elevationColor,
708
+ emissiveIntensity: 0.6,
709
+ roughness: 0.2,
710
+ metalness: 0.8
711
+ })
712
  );
713
  elevationHandle.userData.type = 'elevation';
714
+ elevationHandle.castShadow = true;
715
  scene.add(elevationHandle);
716
 
717
+ // ============================================
718
+ // REFRESH BUTTON (Redesigned)
719
+ // ============================================
720
  const refreshBtn = document.createElement('button');
721
+ refreshBtn.innerHTML = `
722
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
723
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
724
+ <path d="M21 3v5h-5"/>
725
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
726
+ <path d="M8 16H3v5"/>
727
+ </svg>
728
+ <span style="margin-left: 6px;">Reset</span>
729
+ `;
730
+ refreshBtn.style.cssText = `
731
+ position: absolute;
732
+ top: 12px;
733
+ right: 12px;
734
+ background: linear-gradient(135deg, #238636 0%, #2ea043 100%);
735
+ color: white;
736
+ border: none;
737
+ padding: 8px 14px;
738
+ border-radius: 8px;
739
+ cursor: pointer;
740
+ z-index: 10;
741
+ font-size: 13px;
742
+ font-family: 'Segoe UI', system-ui, sans-serif;
743
+ font-weight: 500;
744
+ display: flex;
745
+ align-items: center;
746
+ box-shadow: 0 4px 12px rgba(35, 134, 54, 0.4);
747
+ transition: all 0.2s ease;
748
+ `;
749
+ refreshBtn.onmouseover = () => {
750
+ refreshBtn.style.background = 'linear-gradient(135deg, #2ea043 0%, #3fb950 100%)';
751
+ refreshBtn.style.transform = 'translateY(-1px)';
752
+ refreshBtn.style.boxShadow = '0 6px 16px rgba(35, 134, 54, 0.5)';
753
+ };
754
+ refreshBtn.onmouseout = () => {
755
+ refreshBtn.style.background = 'linear-gradient(135deg, #238636 0%, #2ea043 100%)';
756
+ refreshBtn.style.transform = 'translateY(0)';
757
+ refreshBtn.style.boxShadow = '0 4px 12px rgba(35, 134, 54, 0.4)';
758
+ };
759
+ refreshBtn.onmousedown = () => {
760
+ refreshBtn.style.transform = 'scale(0.95)';
761
+ };
762
+ refreshBtn.onmouseup = () => {
763
+ refreshBtn.style.transform = 'scale(1)';
764
+ };
765
  wrapper.appendChild(refreshBtn);
766
 
767
  refreshBtn.addEventListener('click', () => {
 
783
  lightGroup.position.set(lightX, lightY, lightZ);
784
  lightGroup.lookAt(CENTER);
785
 
786
+ azimuthHandle.position.set(
787
+ AZIMUTH_RADIUS * Math.sin(azRad),
788
+ 0.05,
789
+ AZIMUTH_RADIUS * Math.cos(azRad)
790
+ );
791
+ elevationHandle.position.set(
792
+ -0.8,
793
+ ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y,
794
+ ELEVATION_RADIUS * Math.cos(elRad)
795
+ );
796
 
797
  // Update prompt
798
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
799
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
800
+ let prompt = '💡 Light source from';
801
  if (elSnap !== 0) {
802
  prompt += ' ' + elevationNames[String(elSnap)];
803
  } else {
804
  prompt += ' the ' + azimuthNames[azSnap];
805
  }
806
  promptOverlay.textContent = prompt;
807
+
808
+ // Pulse effect on diffuser based on position
809
+ const pulseIntensity = 1.2 + Math.sin(Date.now() * 0.003) * 0.3;
810
+ diffuserMat.emissiveIntensity = pulseIntensity;
811
  }
812
 
813
  function updatePropsAndTrigger() {
 
823
  const mouse = new THREE.Vector2();
824
  let isDragging = false;
825
  let dragTarget = null;
 
826
  const intersection = new THREE.Vector3();
827
 
828
  const canvas = renderer.domElement;
 
838
  if (intersects.length > 0) {
839
  isDragging = true;
840
  dragTarget = intersects[0].object;
841
+ dragTarget.material.emissiveIntensity = 1.2;
842
+ dragTarget.scale.setScalar(1.4);
 
843
  canvas.style.cursor = 'grabbing';
844
  }
845
  });
 
859
  if (azimuthAngle < 0) azimuthAngle += 360;
860
  }
861
  } else if (dragTarget.userData.type === 'elevation') {
862
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0.8);
863
  if (raycaster.ray.intersectPlane(plane, intersection)) {
864
  const relY = intersection.y - CENTER.y;
865
  const relZ = intersection.z;
866
+ elevationAngle = THREE.MathUtils.clamp(
867
+ THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)),
868
+ -90,
869
+ 90
870
+ );
871
  }
872
  }
873
  updatePositions();
 
875
  raycaster.setFromCamera(mouse, camera);
876
  const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
877
  [azimuthHandle, elevationHandle].forEach(h => {
878
+ h.material.emissiveIntensity = 0.6;
879
  h.scale.setScalar(1);
880
  });
881
  if (intersects.length > 0) {
882
+ intersects[0].object.material.emissiveIntensity = 0.9;
883
+ intersects[0].object.scale.setScalar(1.15);
884
  canvas.style.cursor = 'grab';
885
  } else {
886
  canvas.style.cursor = 'default';
 
890
 
891
  const onMouseUp = () => {
892
  if (dragTarget) {
893
+ dragTarget.material.emissiveIntensity = 0.6;
894
  dragTarget.scale.setScalar(1);
895
 
896
  // Snap and animate
 
926
 
927
  canvas.addEventListener('mouseup', onMouseUp);
928
  canvas.addEventListener('mouseleave', onMouseUp);
929
+
930
  // Touch support
931
  canvas.addEventListener('touchstart', (e) => {
932
  e.preventDefault();
 
941
  if (intersects.length > 0) {
942
  isDragging = true;
943
  dragTarget = intersects[0].object;
944
+ dragTarget.material.emissiveIntensity = 1.2;
945
+ dragTarget.scale.setScalar(1.4);
 
946
  }
947
  }, { passive: false });
948
 
 
955
 
956
  if (isDragging && dragTarget) {
957
  raycaster.setFromCamera(mouse, camera);
958
+
959
  if (dragTarget.userData.type === 'azimuth') {
960
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -0.05);
961
  if (raycaster.ray.intersectPlane(plane, intersection)) {
 
963
  if (azimuthAngle < 0) azimuthAngle += 360;
964
  }
965
  } else if (dragTarget.userData.type === 'elevation') {
966
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0.8);
967
  if (raycaster.ray.intersectPlane(plane, intersection)) {
968
  const relY = intersection.y - CENTER.y;
969
  const relZ = intersection.z;
970
+ elevationAngle = THREE.MathUtils.clamp(
971
+ THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)),
972
+ -90,
973
+ 90
974
+ );
975
  }
976
  }
977
  updatePositions();
 
982
  e.preventDefault();
983
  onMouseUp();
984
  }, { passive: false });
985
+
986
  canvas.addEventListener('touchcancel', (e) => {
987
  e.preventDefault();
988
  onMouseUp();
 
991
  // Initial update
992
  updatePositions();
993
 
994
+ // Render loop with animation
995
+ let time = 0;
996
  function render() {
997
  requestAnimationFrame(render);
998
+ time += 0.016;
999
+
1000
+ // Subtle glow animation
1001
+ if (glowMat) {
1002
+ glowMat.opacity = 0.2 + Math.sin(time * 2) * 0.1;
1003
+ }
1004
+
1005
+ // Indicator light blink
1006
+ if (indicatorLight) {
1007
+ indicatorLight.material.color.setHex(
1008
+ Math.sin(time * 3) > 0 ? 0x00ff00 : 0x004400
1009
+ );
1010
+ }
1011
+
1012
  renderer.render(scene, camera);
1013
  }
1014
  render();
 
1020
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
1021
  }).observe(wrapper);
1022
 
1023
+ // Store update functions
1024
  wrapper._updateFromProps = (newVal) => {
1025
  if (newVal && typeof newVal === 'object') {
1026
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
 
1063
  **kwargs
1064
  )
1065
 
 
 
1066
  css = '''
1067
  #col-container { max-width: 1200px; margin: 0 auto; }
1068
  .dark .progress-text { color: white !important; }
 
1070
  .slider-row { display: flex; gap: 10px; align-items: center; }
1071
  #main-title h1 {font-size: 2.4em !important;}
1072
  '''
1073
+
1074
  with gr.Blocks(css=css) as demo:
1075
  gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
1076
  gr.Markdown("Control lighting directions using the **3D viewport** or **sliders**. Use [Multi-Angle-Lighting](https://huggingface.co/dx8152/Qwen-Edit-2509-Multi-Angle-Lighting) LoRA for precise lighting control, paired with [Rapid-AIO-V19](https://huggingface.co/prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19).")
 
1091
  gr.Markdown("### Slider Controls")
1092
 
1093
  azimuth_slider = gr.Slider(
1094
+ label="🟡 Azimuth (Horizontal Rotation)",
1095
  minimum=0,
1096
  maximum=315,
1097
  step=45,
 
1100
  )
1101
 
1102
  elevation_slider = gr.Slider(
1103
+ label="🔵 Elevation (Vertical Angle)",
1104
  minimum=-90,
1105
  maximum=90,
1106
  step=90,
 
1149
  """Update the 3D component with the uploaded image."""
1150
  if image is None:
1151
  return gr.update(imageUrl=None)
1152
+
1153
  import base64
1154
  from io import BytesIO
1155
  buffered = BytesIO()