prithivMLmods commited on
Commit
4f2e962
·
verified ·
1 Parent(s): 0899752

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -302
app.py CHANGED
@@ -141,6 +141,7 @@ def build_lighting_prompt(azimuth: float, elevation: float) -> str:
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()))
146
 
@@ -224,17 +225,11 @@ class LightingControl3D(gr.HTML):
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
  """
@@ -243,7 +238,10 @@ class LightingControl3D(gr.HTML):
243
  (() => {
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);
@@ -254,7 +252,7 @@ class LightingControl3D(gr.HTML):
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);
@@ -273,52 +271,39 @@ class LightingControl3D(gr.HTML):
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);
319
- const BASE_DISTANCE = 2.5;
320
- const AZIMUTH_RADIUS = 2.4;
321
- const ELEVATION_RADIUS = 1.8;
322
 
323
  // State
324
  let azimuthAngle = props.value?.azimuth || 0;
@@ -345,7 +330,7 @@ class LightingControl3D(gr.HTML):
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');
@@ -355,27 +340,27 @@ class LightingControl3D(gr.HTML):
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
 
@@ -390,17 +375,22 @@ class LightingControl3D(gr.HTML):
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
@@ -409,16 +399,16 @@ class LightingControl3D(gr.HTML):
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
 
@@ -433,7 +423,7 @@ class LightingControl3D(gr.HTML):
433
  const img = texture.image;
434
  if (img && img.width && img.height) {
435
  const aspect = img.width / img.height;
436
- const maxSize = 1.5;
437
  let planeWidth, planeHeight;
438
  if (aspect > 1) {
439
  planeWidth = maxSize;
@@ -443,7 +433,6 @@ class LightingControl3D(gr.HTML):
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
@@ -453,12 +442,10 @@ class LightingControl3D(gr.HTML):
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,53 +456,112 @@ class LightingControl3D(gr.HTML):
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,
@@ -524,62 +570,45 @@ class LightingControl3D(gr.HTML):
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;
@@ -588,10 +617,10 @@ class LightingControl3D(gr.HTML):
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);
@@ -600,70 +629,73 @@ class LightingControl3D(gr.HTML):
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
  ));
@@ -672,95 +704,98 @@ class LightingControl3D(gr.HTML):
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
 
@@ -769,6 +804,12 @@ class LightingControl3D(gr.HTML):
769
  elevationAngle = 0;
770
  updatePositions();
771
  updatePropsAndTrigger();
 
 
 
 
 
 
772
  });
773
 
774
  function updatePositions() {
@@ -783,31 +824,33 @@ class LightingControl3D(gr.HTML):
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() {
@@ -818,7 +861,7 @@ class LightingControl3D(gr.HTML):
818
  trigger('change', props.value);
819
  }
820
 
821
- // Raycasting
822
  const raycaster = new THREE.Raycaster();
823
  const mouse = new THREE.Vector2();
824
  let isDragging = false;
@@ -827,19 +870,33 @@ class LightingControl3D(gr.HTML):
827
 
828
  const canvas = renderer.domElement;
829
 
 
 
 
 
830
  canvas.addEventListener('mousedown', (e) => {
831
  const rect = canvas.getBoundingClientRect();
832
  mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
833
  mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
834
 
835
  raycaster.setFromCamera(mouse, camera);
836
- const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
 
 
 
837
 
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,7 +916,7 @@ class LightingControl3D(gr.HTML):
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;
@@ -873,14 +930,26 @@ class LightingControl3D(gr.HTML):
873
  updatePositions();
874
  } else {
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,10 +959,11 @@ class LightingControl3D(gr.HTML):
890
 
891
  const onMouseUp = () => {
892
  if (dragTarget) {
893
- dragTarget.material.emissiveIntensity = 0.6;
 
894
  dragTarget.scale.setScalar(1);
895
 
896
- // Snap and animate
897
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
898
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
899
 
@@ -936,13 +1006,21 @@ class LightingControl3D(gr.HTML):
936
  mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
937
 
938
  raycaster.setFromCamera(mouse, camera);
939
- const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
 
 
 
940
 
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
 
@@ -963,7 +1041,7 @@ class LightingControl3D(gr.HTML):
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;
@@ -991,23 +1069,14 @@ class LightingControl3D(gr.HTML):
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
  }
@@ -1070,7 +1139,6 @@ css = '''
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).")
 
141
  Returns:
142
  Formatted prompt string for the LoRA
143
  """
144
+ # Snap to nearest valid values
145
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
146
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
147
 
 
225
  value = {"azimuth": 0, "elevation": 0}
226
 
227
  html_template = """
228
+ <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: 2px solid #30363d;">
229
+ <div id="prompt-overlay" style="position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(255,69,0,0.9), rgba(220,53,69,0.9)); padding: 10px 20px; border-radius: 20px; font-family: 'Segoe UI', sans-serif; font-size: 13px; font-weight: 600; color: white; white-space: nowrap; z-index: 10; box-shadow: 0 4px 15px rgba(255,69,0,0.4); border: 1px solid rgba(255,255,255,0.2);"></div>
230
+ <div id="info-panel" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.7); padding: 8px 12px; border-radius: 8px; font-family: 'Segoe UI', sans-serif; font-size: 11px; color: #8b949e; z-index: 10; border: 1px solid #30363d;">
231
+ <div style="margin-bottom: 4px;"><span style="color: #ffd700;">●</span> Azimuth: <span id="az-value" style="color: #ffd700;">0°</span></div>
232
+ <div><span style="color: #4a9eff;">●</span> Elevation: <span id="el-value" style="color: #4a9eff;">0°</span></div>
 
 
 
 
 
 
233
  </div>
234
  </div>
235
  """
 
238
  (() => {
239
  const wrapper = element.querySelector('#lighting-control-wrapper');
240
  const promptOverlay = element.querySelector('#prompt-overlay');
241
+ const azValueDisplay = element.querySelector('#az-value');
242
+ const elValueDisplay = element.querySelector('#el-value');
243
 
244
+ // Wait for THREE to load
245
  const initScene = () => {
246
  if (typeof THREE === 'undefined') {
247
  setTimeout(initScene, 100);
 
252
  const scene = new THREE.Scene();
253
  scene.background = new THREE.Color(0x0d1117);
254
 
255
+ // Add fog for depth effect
256
  scene.fog = new THREE.Fog(0x0d1117, 8, 20);
257
 
258
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
 
271
  // Ambient lighting
272
  scene.add(new THREE.AmbientLight(0x404050, 0.3));
273
 
274
+ // Ground plane with better material
275
+ const groundGeometry = new THREE.PlaneGeometry(12, 12);
276
+ const groundMaterial = new THREE.MeshStandardMaterial({
277
+ color: 0x1a1f26,
278
+ roughness: 0.9,
279
+ metalness: 0.1
280
+ });
281
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
 
282
  ground.rotation.x = -Math.PI / 2;
283
  ground.position.y = 0;
284
  ground.receiveShadow = true;
285
  scene.add(ground);
286
 
287
+ // Shadow receiving plane (invisible)
288
+ const shadowPlane = new THREE.Mesh(
289
+ new THREE.PlaneGeometry(12, 12),
290
+ new THREE.ShadowMaterial({ opacity: 0.5 })
291
+ );
292
+ shadowPlane.rotation.x = -Math.PI / 2;
293
+ shadowPlane.position.y = 0.001;
294
+ shadowPlane.receiveShadow = true;
295
+ scene.add(shadowPlane);
 
 
 
 
296
 
297
+ // Enhanced grid
298
+ const gridHelper = new THREE.GridHelper(10, 20, 0x30363d, 0x21262d);
299
+ gridHelper.position.y = 0.002;
300
+ scene.add(gridHelper);
 
 
 
 
 
 
 
 
301
 
302
  // Constants
303
  const CENTER = new THREE.Vector3(0, 0.75, 0);
304
+ const BASE_DISTANCE = 2.8;
305
+ const AZIMUTH_RADIUS = 2.6;
306
+ const ELEVATION_RADIUS = 2.0;
307
 
308
  // State
309
  let azimuthAngle = props.value?.azimuth || 0;
 
330
  canvas.height = 256;
331
  const ctx = canvas.getContext('2d');
332
 
333
+ // Background gradient
334
  const gradient = ctx.createLinearGradient(0, 0, 256, 256);
335
  gradient.addColorStop(0, '#2d333b');
336
  gradient.addColorStop(1, '#22272e');
 
340
  // Image icon
341
  ctx.strokeStyle = '#484f58';
342
  ctx.lineWidth = 3;
343
+ ctx.strokeRect(60, 70, 136, 116);
 
 
344
 
345
+ // Mountain shapes
346
  ctx.fillStyle = '#484f58';
347
  ctx.beginPath();
348
+ ctx.moveTo(70, 170);
349
+ ctx.lineTo(110, 120);
350
+ ctx.lineTo(150, 160);
351
+ ctx.lineTo(180, 130);
352
+ ctx.lineTo(186, 170);
353
  ctx.closePath();
354
  ctx.fill();
355
 
356
+ // Sun circle
357
  ctx.beginPath();
358
+ ctx.arc(160, 100, 15, 0, Math.PI * 2);
359
  ctx.fill();
360
 
361
  // Text
362
  ctx.fillStyle = '#6e7681';
363
+ ctx.font = '14px Arial';
364
  ctx.textAlign = 'center';
365
  ctx.fillText('Upload Image', 128, 210);
366
 
 
375
  roughness: 0.3,
376
  metalness: 0.1
377
  });
378
+ let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), planeMaterial);
379
  targetPlane.position.copy(CENTER);
380
  targetPlane.receiveShadow = true;
381
  targetPlane.castShadow = true;
382
  scene.add(targetPlane);
383
 
384
+ // Add frame around image
385
+ const frameGeometry = new THREE.BoxGeometry(1.5, 1.5, 0.05);
386
+ const frameMaterial = new THREE.MeshStandardMaterial({
387
+ color: 0x30363d,
388
+ roughness: 0.5,
389
+ metalness: 0.3
390
+ });
391
+ const frame = new THREE.Mesh(frameGeometry, frameMaterial);
392
+ frame.position.copy(CENTER);
393
+ frame.position.z -= 0.03;
394
  scene.add(frame);
395
 
396
  // Function to update texture from image URL
 
399
  planeMaterial.map = createPlaceholderTexture();
400
  planeMaterial.needsUpdate = true;
401
  scene.remove(targetPlane);
402
+ targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), planeMaterial);
 
403
  targetPlane.position.copy(CENTER);
404
  targetPlane.receiveShadow = true;
405
  targetPlane.castShadow = true;
406
  scene.add(targetPlane);
407
 
408
+ frame.geometry.dispose();
409
+ frame.geometry = new THREE.BoxGeometry(1.5, 1.5, 0.05);
410
+ frame.position.copy(CENTER);
411
+ frame.position.z -= 0.03;
412
  return;
413
  }
414
 
 
423
  const img = texture.image;
424
  if (img && img.width && img.height) {
425
  const aspect = img.width / img.height;
426
+ const maxSize = 1.6;
427
  let planeWidth, planeHeight;
428
  if (aspect > 1) {
429
  planeWidth = maxSize;
 
433
  planeWidth = maxSize * aspect;
434
  }
435
  scene.remove(targetPlane);
 
436
  targetPlane = new THREE.Mesh(
437
  new THREE.PlaneGeometry(planeWidth, planeHeight),
438
  planeMaterial
 
442
  targetPlane.castShadow = true;
443
  scene.add(targetPlane);
444
 
445
+ frame.geometry.dispose();
446
+ frame.geometry = new THREE.BoxGeometry(planeWidth + 0.1, planeHeight + 0.1, 0.05);
447
+ frame.position.copy(CENTER);
448
+ frame.position.z -= 0.03;
 
 
449
  }
450
  }, undefined, (err) => {
451
  console.error('Failed to load texture:', err);
 
456
  updateTextureFromUrl(props.imageUrl);
457
  }
458
 
459
+ // ========== SOFTBOX LIGHT DESIGN ==========
 
 
460
  const lightGroup = new THREE.Group();
461
 
462
+ // Softbox housing - Red color
463
+ const softboxDepth = 0.4;
464
+ const softboxWidth = 0.8;
465
+ const softboxHeight = 0.8;
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  // Back panel (red)
468
  const backPanel = new THREE.Mesh(
469
+ new THREE.BoxGeometry(softboxWidth, softboxHeight, 0.05),
470
+ new THREE.MeshStandardMaterial({
471
+ color: 0xcc0000,
472
+ roughness: 0.3,
473
+ metalness: 0.6
474
+ })
475
  );
476
+ backPanel.position.z = -softboxDepth / 2;
 
477
  lightGroup.add(backPanel);
478
 
479
+ // Side panels (red, tapered)
480
+ const createSidePanel = (width, height, rotY, posX, posZ) => {
481
+ const shape = new THREE.Shape();
482
+ shape.moveTo(-width/2, -height/2);
483
+ shape.lineTo(width/2, -height/2 * 0.7);
484
+ shape.lineTo(width/2, height/2 * 0.7);
485
+ shape.lineTo(-width/2, height/2);
486
+ shape.closePath();
487
+
488
+ const geometry = new THREE.ExtrudeGeometry(shape, { depth: 0.02, bevelEnabled: false });
489
+ const panel = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
490
+ color: 0xaa0000,
491
+ roughness: 0.4,
492
+ metalness: 0.5,
493
+ side: THREE.DoubleSide
494
+ }));
495
+ panel.rotation.y = rotY;
496
+ panel.position.set(posX, 0, posZ);
497
+ return panel;
498
+ };
499
+
500
+ // Left side
501
+ const leftSide = new THREE.Mesh(
502
+ new THREE.PlaneGeometry(softboxDepth, softboxHeight),
503
+ new THREE.MeshStandardMaterial({
504
+ color: 0xaa0000,
505
+ roughness: 0.4,
506
+ metalness: 0.5,
507
+ side: THREE.DoubleSide
508
+ })
509
+ );
510
+ leftSide.rotation.y = Math.PI / 2;
511
+ leftSide.position.x = -softboxWidth / 2;
512
+ leftSide.position.z = -softboxDepth / 4;
513
+ lightGroup.add(leftSide);
514
+
515
+ // Right side
516
+ const rightSide = leftSide.clone();
517
+ rightSide.position.x = softboxWidth / 2;
518
+ lightGroup.add(rightSide);
519
+
520
+ // Top side
521
+ const topSide = new THREE.Mesh(
522
+ new THREE.PlaneGeometry(softboxWidth, softboxDepth),
523
+ new THREE.MeshStandardMaterial({
524
+ color: 0xaa0000,
525
+ roughness: 0.4,
526
+ metalness: 0.5,
527
+ side: THREE.DoubleSide
528
  })
529
  );
530
+ topSide.rotation.x = Math.PI / 2;
531
+ topSide.position.y = softboxHeight / 2;
532
+ topSide.position.z = -softboxDepth / 4;
533
+ lightGroup.add(topSide);
534
+
535
+ // Bottom side
536
+ const bottomSide = topSide.clone();
537
+ bottomSide.position.y = -softboxHeight / 2;
538
+ lightGroup.add(bottomSide);
539
+
540
+ // Corner edges (red tubes)
541
+ const edgeMaterial = new THREE.MeshStandardMaterial({
542
+ color: 0xdd0000,
543
+ roughness: 0.3,
544
+ metalness: 0.7
545
+ });
546
+ const edgeRadius = 0.02;
547
 
548
+ // Vertical edges
549
+ const verticalEdgeGeom = new THREE.CylinderGeometry(edgeRadius, edgeRadius, softboxHeight, 8);
550
+ const corners = [
551
+ [-softboxWidth/2, 0, 0],
552
+ [softboxWidth/2, 0, 0],
553
+ [-softboxWidth/2, 0, -softboxDepth/2],
554
+ [softboxWidth/2, 0, -softboxDepth/2]
555
+ ];
556
+ corners.forEach(pos => {
557
+ const edge = new THREE.Mesh(verticalEdgeGeom, edgeMaterial);
558
+ edge.position.set(pos[0], pos[1], pos[2]);
559
+ lightGroup.add(edge);
560
+ });
561
+
562
+ // Diffusion panel (WHITE - the light source)
563
+ const diffuserGeometry = new THREE.PlaneGeometry(softboxWidth - 0.05, softboxHeight - 0.05);
564
+ const diffuserMaterial = new THREE.MeshStandardMaterial({
565
  color: 0xffffff,
566
  emissive: 0xffffff,
567
  emissiveIntensity: 1.5,
 
570
  transparent: true,
571
  opacity: 0.95
572
  });
573
+ const diffuser = new THREE.Mesh(diffuserGeometry, diffuserMaterial);
574
+ diffuser.position.z = 0.01;
575
+ lightGroup.add(diffuser);
576
 
577
+ // Inner glow ring
578
+ const glowRing = new THREE.Mesh(
579
+ new THREE.RingGeometry(0.3, 0.35, 32),
580
+ new THREE.MeshBasicMaterial({
581
+ color: 0xffffee,
582
+ transparent: true,
583
+ opacity: 0.8
584
+ })
585
  );
586
+ glowRing.position.z = 0.02;
587
+ lightGroup.add(glowRing);
588
 
589
+ // Mounting bracket (on back)
590
+ const bracketGeom = new THREE.BoxGeometry(0.15, 0.15, 0.2);
591
+ const bracketMat = new THREE.MeshStandardMaterial({
592
+ color: 0x222222,
593
+ roughness: 0.5,
594
+ metalness: 0.8
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  });
596
+ const bracket = new THREE.Mesh(bracketGeom, bracketMat);
597
+ bracket.position.z = -softboxDepth / 2 - 0.1;
 
 
 
 
598
  lightGroup.add(bracket);
599
 
600
  // Stand connection
601
  const standPole = new THREE.Mesh(
602
+ new THREE.CylinderGeometry(0.03, 0.03, 0.3, 8),
603
  bracketMat
604
  );
605
  standPole.rotation.x = Math.PI / 2;
606
+ standPole.position.z = -softboxDepth / 2 - 0.25;
607
  lightGroup.add(standPole);
608
 
609
+ // The actual spotlight
610
+ const spotLight = new THREE.SpotLight(0xffffff, 15, 12, Math.PI / 4, 0.8, 1);
611
+ spotLight.position.set(0, 0, 0.1);
 
 
 
 
 
 
 
 
612
  spotLight.castShadow = true;
613
  spotLight.shadow.mapSize.width = 1024;
614
  spotLight.shadow.mapSize.height = 1024;
 
617
  spotLight.shadow.bias = -0.002;
618
  lightGroup.add(spotLight);
619
 
620
+ // Point light for soft fill
621
+ const fillLight = new THREE.PointLight(0xfff5ee, 2, 8);
622
+ fillLight.position.set(0, 0, 0.2);
623
+ lightGroup.add(fillLight);
624
 
625
  const lightTarget = new THREE.Object3D();
626
  lightTarget.position.copy(CENTER);
 
629
 
630
  scene.add(lightGroup);
631
 
632
+ // ========== AZIMUTH RING (YELLOW) ==========
 
 
 
 
633
  const azimuthRing = new THREE.Mesh(
634
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.035, 16, 64),
635
  new THREE.MeshStandardMaterial({
636
+ color: 0xffd700,
637
+ emissive: 0xffd700,
638
+ emissiveIntensity: 0.2,
639
  roughness: 0.3,
640
+ metalness: 0.6
641
  })
642
  );
643
  azimuthRing.rotation.x = Math.PI / 2;
644
  azimuthRing.position.y = 0.05;
645
  scene.add(azimuthRing);
646
 
647
+ // Azimuth tick marks
648
+ for (let i = 0; i < 8; i++) {
649
+ const angle = (i * 45) * Math.PI / 180;
650
+ const tickGeom = new THREE.BoxGeometry(0.08, 0.02, 0.02);
651
+ const tickMat = new THREE.MeshStandardMaterial({
652
+ color: 0xffd700,
653
+ emissive: 0xffd700,
654
+ emissiveIntensity: 0.3
655
+ });
656
+ const tick = new THREE.Mesh(tickGeom, tickMat);
657
+ tick.position.set(
658
+ (AZIMUTH_RADIUS + 0.1) * Math.sin(angle),
 
 
 
659
  0.05,
660
+ (AZIMUTH_RADIUS + 0.1) * Math.cos(angle)
661
  );
662
+ tick.rotation.y = -angle;
663
+ scene.add(tick);
664
+ }
665
 
666
+ // Azimuth handle (YELLOW)
667
+ const azimuthHandleGroup = new THREE.Group();
668
  const azimuthHandle = new THREE.Mesh(
669
+ new THREE.SphereGeometry(0.15, 24, 24),
670
  new THREE.MeshStandardMaterial({
671
+ color: 0xffd700,
672
+ emissive: 0xffd700,
673
+ emissiveIntensity: 0.4,
674
  roughness: 0.2,
675
+ metalness: 0.7
676
  })
677
  );
678
+ azimuthHandleGroup.add(azimuthHandle);
 
 
679
 
680
+ // Handle ring decoration
681
+ const azHandleRing = new THREE.Mesh(
682
+ new THREE.TorusGeometry(0.18, 0.02, 8, 24),
683
+ new THREE.MeshStandardMaterial({
684
+ color: 0xffee00,
685
+ emissive: 0xffee00,
686
+ emissiveIntensity: 0.3
687
+ })
688
+ );
689
+ azimuthHandleGroup.add(azHandleRing);
690
+ azimuthHandleGroup.userData.type = 'azimuth';
691
+ scene.add(azimuthHandleGroup);
692
 
693
+ // ========== ELEVATION ARC (BLUE) ==========
694
  const arcPoints = [];
695
+ for (let i = 0; i <= 32; i++) {
696
+ const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
697
  arcPoints.push(new THREE.Vector3(
698
+ -1.0,
699
  ELEVATION_RADIUS * Math.sin(angle) + CENTER.y,
700
  ELEVATION_RADIUS * Math.cos(angle)
701
  ));
 
704
  const elevationArc = new THREE.Mesh(
705
  new THREE.TubeGeometry(arcCurve, 48, 0.035, 8, false),
706
  new THREE.MeshStandardMaterial({
707
+ color: 0x4a9eff,
708
+ emissive: 0x4a9eff,
709
+ emissiveIntensity: 0.2,
710
  roughness: 0.3,
711
+ metalness: 0.6
712
  })
713
  );
714
  scene.add(elevationArc);
715
 
716
+ // Elevation tick marks
717
+ [-90, 0, 90].forEach(deg => {
718
+ const angle = deg * Math.PI / 180;
719
+ const tickGeom = new THREE.BoxGeometry(0.02, 0.08, 0.02);
720
+ const tickMat = new THREE.MeshStandardMaterial({
721
+ color: 0x4a9eff,
722
+ emissive: 0x4a9eff,
723
+ emissiveIntensity: 0.3
724
+ });
725
+ const tick = new THREE.Mesh(tickGeom, tickMat);
726
+ tick.position.set(
727
+ -1.0,
728
+ (ELEVATION_RADIUS + 0.1) * Math.sin(angle) + CENTER.y,
729
+ (ELEVATION_RADIUS + 0.1) * Math.cos(angle)
 
730
  );
731
+ tick.rotation.x = -angle;
732
+ scene.add(tick);
733
  });
734
 
735
+ // Elevation handle (BLUE)
736
+ const elevationHandleGroup = new THREE.Group();
737
  const elevationHandle = new THREE.Mesh(
738
+ new THREE.SphereGeometry(0.15, 24, 24),
739
  new THREE.MeshStandardMaterial({
740
+ color: 0x4a9eff,
741
+ emissive: 0x4a9eff,
742
+ emissiveIntensity: 0.4,
743
  roughness: 0.2,
744
+ metalness: 0.7
745
  })
746
  );
747
+ elevationHandleGroup.add(elevationHandle);
 
 
748
 
749
+ // Handle ring decoration
750
+ const elHandleRing = new THREE.Mesh(
751
+ new THREE.TorusGeometry(0.18, 0.02, 8, 24),
752
+ new THREE.MeshStandardMaterial({
753
+ color: 0x66b3ff,
754
+ emissive: 0x66b3ff,
755
+ emissiveIntensity: 0.3
756
+ })
757
+ );
758
+ elevationHandleGroup.add(elHandleRing);
759
+ elevationHandleGroup.userData.type = 'elevation';
760
+ scene.add(elevationHandleGroup);
761
+
762
+ // ========== REFRESH BUTTON ==========
763
  const refreshBtn = document.createElement('button');
764
  refreshBtn.innerHTML = `
765
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
766
+ <path d="M23 4v6h-6"></path>
767
+ <path d="M1 20v-6h6"></path>
768
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
 
769
  </svg>
770
  <span style="margin-left: 6px;">Reset</span>
771
  `;
772
  refreshBtn.style.cssText = `
773
  position: absolute;
774
+ top: 10px;
775
+ right: 10px;
776
+ background: linear-gradient(135deg, #ff4500, #dc3545);
777
  color: white;
778
  border: none;
779
  padding: 8px 14px;
780
  border-radius: 8px;
781
  cursor: pointer;
782
  z-index: 10;
783
+ font-size: 12px;
784
+ font-weight: 600;
785
+ font-family: 'Segoe UI', sans-serif;
786
  display: flex;
787
  align-items: center;
788
+ box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
789
  transition: all 0.2s ease;
790
+ border: 1px solid rgba(255,255,255,0.2);
791
  `;
792
+ refreshBtn.onmouseenter = () => {
793
+ refreshBtn.style.transform = 'scale(1.05)';
794
+ refreshBtn.style.boxShadow = '0 6px 16px rgba(255, 69, 0, 0.5)';
 
 
 
 
 
 
795
  };
796
+ refreshBtn.onmouseleave = () => {
 
 
 
797
  refreshBtn.style.transform = 'scale(1)';
798
+ refreshBtn.style.boxShadow = '0 4px 12px rgba(255, 69, 0, 0.4)';
799
  };
800
  wrapper.appendChild(refreshBtn);
801
 
 
804
  elevationAngle = 0;
805
  updatePositions();
806
  updatePropsAndTrigger();
807
+
808
+ // Button click animation
809
+ refreshBtn.style.transform = 'scale(0.95)';
810
+ setTimeout(() => {
811
+ refreshBtn.style.transform = 'scale(1)';
812
+ }, 100);
813
  });
814
 
815
  function updatePositions() {
 
824
  lightGroup.position.set(lightX, lightY, lightZ);
825
  lightGroup.lookAt(CENTER);
826
 
827
+ azimuthHandleGroup.position.set(
828
  AZIMUTH_RADIUS * Math.sin(azRad),
829
  0.05,
830
  AZIMUTH_RADIUS * Math.cos(azRad)
831
  );
832
+
833
+ elevationHandleGroup.position.set(
834
+ -1.0,
835
  ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y,
836
  ELEVATION_RADIUS * Math.cos(elRad)
837
  );
838
 
839
+ // Update info panel
840
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
841
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
842
+
843
+ azValueDisplay.textContent = azSnap + '° (' + azimuthNames[azSnap] + ')';
844
+ elValueDisplay.textContent = elSnap + '°' + (elSnap !== 0 ? ' (' + elevationNames[String(elSnap)] + ')' : '');
845
+
846
+ // Update prompt
847
+ let prompt = 'Light source from';
848
  if (elSnap !== 0) {
849
  prompt += ' ' + elevationNames[String(elSnap)];
850
  } else {
851
  prompt += ' the ' + azimuthNames[azSnap];
852
  }
853
  promptOverlay.textContent = prompt;
 
 
 
 
854
  }
855
 
856
  function updatePropsAndTrigger() {
 
861
  trigger('change', props.value);
862
  }
863
 
864
+ // Raycasting for handle interaction
865
  const raycaster = new THREE.Raycaster();
866
  const mouse = new THREE.Vector2();
867
  let isDragging = false;
 
870
 
871
  const canvas = renderer.domElement;
872
 
873
+ function getHandleFromGroup(group) {
874
+ return group.children[0]; // The sphere is the first child
875
+ }
876
+
877
  canvas.addEventListener('mousedown', (e) => {
878
  const rect = canvas.getBoundingClientRect();
879
  mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
880
  mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
881
 
882
  raycaster.setFromCamera(mouse, camera);
883
+ const intersects = raycaster.intersectObjects([
884
+ getHandleFromGroup(azimuthHandleGroup),
885
+ getHandleFromGroup(elevationHandleGroup)
886
+ ], true);
887
 
888
  if (intersects.length > 0) {
889
  isDragging = true;
890
+ // Find parent group
891
+ let obj = intersects[0].object;
892
+ while (obj.parent && !obj.userData.type) {
893
+ obj = obj.parent;
894
+ }
895
+ dragTarget = obj;
896
+
897
+ const handle = getHandleFromGroup(dragTarget);
898
+ handle.material.emissiveIntensity = 0.8;
899
+ dragTarget.scale.setScalar(1.3);
900
  canvas.style.cursor = 'grabbing';
901
  }
902
  });
 
916
  if (azimuthAngle < 0) azimuthAngle += 360;
917
  }
918
  } else if (dragTarget.userData.type === 'elevation') {
919
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 1.0);
920
  if (raycaster.ray.intersectPlane(plane, intersection)) {
921
  const relY = intersection.y - CENTER.y;
922
  const relZ = intersection.z;
 
930
  updatePositions();
931
  } else {
932
  raycaster.setFromCamera(mouse, camera);
933
+ const intersects = raycaster.intersectObjects([
934
+ getHandleFromGroup(azimuthHandleGroup),
935
+ getHandleFromGroup(elevationHandleGroup)
936
+ ], true);
937
+
938
+ // Reset all handles
939
+ [azimuthHandleGroup, elevationHandleGroup].forEach(group => {
940
+ const handle = getHandleFromGroup(group);
941
+ handle.material.emissiveIntensity = 0.4;
942
+ group.scale.setScalar(1);
943
  });
944
+
945
  if (intersects.length > 0) {
946
+ let obj = intersects[0].object;
947
+ while (obj.parent && !obj.userData.type) {
948
+ obj = obj.parent;
949
+ }
950
+ const handle = getHandleFromGroup(obj);
951
+ handle.material.emissiveIntensity = 0.6;
952
+ obj.scale.setScalar(1.15);
953
  canvas.style.cursor = 'grab';
954
  } else {
955
  canvas.style.cursor = 'default';
 
959
 
960
  const onMouseUp = () => {
961
  if (dragTarget) {
962
+ const handle = getHandleFromGroup(dragTarget);
963
+ handle.material.emissiveIntensity = 0.4;
964
  dragTarget.scale.setScalar(1);
965
 
966
+ // Snap animation
967
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
968
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
969
 
 
1006
  mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
1007
 
1008
  raycaster.setFromCamera(mouse, camera);
1009
+ const intersects = raycaster.intersectObjects([
1010
+ getHandleFromGroup(azimuthHandleGroup),
1011
+ getHandleFromGroup(elevationHandleGroup)
1012
+ ], true);
1013
 
1014
  if (intersects.length > 0) {
1015
  isDragging = true;
1016
+ let obj = intersects[0].object;
1017
+ while (obj.parent && !obj.userData.type) {
1018
+ obj = obj.parent;
1019
+ }
1020
+ dragTarget = obj;
1021
+ const handle = getHandleFromGroup(dragTarget);
1022
+ handle.material.emissiveIntensity = 0.8;
1023
+ dragTarget.scale.setScalar(1.3);
1024
  }
1025
  }, { passive: false });
1026
 
 
1041
  if (azimuthAngle < 0) azimuthAngle += 360;
1042
  }
1043
  } else if (dragTarget.userData.type === 'elevation') {
1044
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 1.0);
1045
  if (raycaster.ray.intersectPlane(plane, intersection)) {
1046
  const relY = intersection.y - CENTER.y;
1047
  const relZ = intersection.z;
 
1069
  // Initial update
1070
  updatePositions();
1071
 
1072
+ // Render loop with subtle animation
1073
  let time = 0;
1074
  function render() {
1075
  requestAnimationFrame(render);
1076
+ time += 0.01;
1077
 
1078
+ // Subtle glow animation on diffuser
1079
+ diffuserMaterial.emissiveIntensity = 1.3 + Math.sin(time * 2) * 0.2;
 
 
 
 
 
 
 
 
 
1080
 
1081
  renderer.render(scene, camera);
1082
  }
 
1139
  .slider-row { display: flex; gap: 10px; align-items: center; }
1140
  #main-title h1 {font-size: 2.4em !important;}
1141
  '''
 
1142
  with gr.Blocks(css=css) as demo:
1143
  gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
1144
  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).")