prithivMLmods commited on
Commit
1c12e0d
·
verified ·
1 Parent(s): 93d6a75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -422
app.py CHANGED
@@ -88,7 +88,7 @@ device = "cuda" if torch.cuda.is_available() else "cpu"
88
  pipe = QwenImageEditPlusPipeline.from_pretrained(
89
  "Qwen/Qwen-Image-Edit-2511",
90
  transformer=QwenImageTransformer2DModel.from_pretrained(
91
- "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V4",
92
  torch_dtype=dtype,
93
  device_map='cuda'
94
  ),
@@ -225,26 +225,8 @@ class LightingControl3D(gr.HTML):
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: 1px solid #30363d;">
229
- <div id="prompt-overlay" style="position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.85); padding: 10px 20px; border-radius: 20px; font-family: 'Segoe 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>
230
- <div id="refresh-btn" style="position: absolute; top: 12px; right: 12px; background: linear-gradient(135deg, #238636 0%, #2ea043 100%); padding: 8px 16px; border-radius: 8px; cursor: pointer; z-index: 10; font-family: 'Segoe UI', sans-serif; font-size: 12px; font-weight: 600; color: white; display: flex; align-items: center; gap: 6px; box-shadow: 0 2px 8px rgba(46,160,67,0.3); transition: all 0.2s ease;">
231
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
232
- <path d="M23 4v6h-6"></path>
233
- <path d="M1 20v-6h6"></path>
234
- <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>
235
- </svg>
236
- Reset
237
- </div>
238
- <div id="info-panel" style="position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.75); padding: 8px 12px; border-radius: 8px; font-family: 'Segoe UI', sans-serif; font-size: 11px; color: #8b949e; z-index: 10; border: 1px solid #30363d;">
239
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
240
- <span style="width: 10px; height: 10px; background: #ffd700; border-radius: 50%; display: inline-block;"></span>
241
- <span>Azimuth (Direction)</span>
242
- </div>
243
- <div style="display: flex; align-items: center; gap: 8px;">
244
- <span style="width: 10px; height: 10px; background: #0088ff; border-radius: 50%; display: inline-block;"></span>
245
- <span>Elevation (Height)</span>
246
- </div>
247
- </div>
248
  </div>
249
  """
250
 
@@ -252,17 +234,6 @@ class LightingControl3D(gr.HTML):
252
  (() => {
253
  const wrapper = element.querySelector('#lighting-control-wrapper');
254
  const promptOverlay = element.querySelector('#prompt-overlay');
255
- const refreshBtn = element.querySelector('#refresh-btn');
256
-
257
- // Hover effects for refresh button
258
- refreshBtn.addEventListener('mouseenter', () => {
259
- refreshBtn.style.transform = 'scale(1.05)';
260
- refreshBtn.style.boxShadow = '0 4px 16px rgba(46,160,67,0.5)';
261
- });
262
- refreshBtn.addEventListener('mouseleave', () => {
263
- refreshBtn.style.transform = 'scale(1)';
264
- refreshBtn.style.boxShadow = '0 2px 8px rgba(46,160,67,0.3)';
265
- });
266
 
267
  // Wait for THREE to load
268
  const initScene = () => {
@@ -273,10 +244,7 @@ class LightingControl3D(gr.HTML):
273
 
274
  // Scene setup
275
  const scene = new THREE.Scene();
276
- scene.background = new THREE.Color(0x0d1117);
277
-
278
- // Add fog for depth
279
- scene.fog = new THREE.Fog(0x0d1117, 8, 20);
280
 
281
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
282
  camera.position.set(4.5, 3, 4.5);
@@ -287,46 +255,29 @@ class LightingControl3D(gr.HTML):
287
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
288
  renderer.shadowMap.enabled = true;
289
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
290
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
291
- renderer.toneMappingExposure = 1.2;
292
- wrapper.insertBefore(renderer.domElement, wrapper.firstChild);
293
 
294
- // Ambient lighting
295
- scene.add(new THREE.AmbientLight(0x404050, 0.3));
296
 
297
- // Ground plane with reflective material
298
- const groundGeometry = new THREE.PlaneGeometry(12, 12);
299
- const groundMaterial = new THREE.MeshStandardMaterial({
300
- color: 0x1a1a2e,
301
- roughness: 0.8,
302
- metalness: 0.2
303
- });
304
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
305
  ground.rotation.x = -Math.PI / 2;
306
  ground.position.y = 0;
307
  ground.receiveShadow = true;
308
  scene.add(ground);
309
 
310
- // Shadow plane
311
- const shadowPlane = new THREE.Mesh(
312
- new THREE.PlaneGeometry(10, 10),
313
- new THREE.ShadowMaterial({ opacity: 0.5 })
314
- );
315
- shadowPlane.rotation.x = -Math.PI / 2;
316
- shadowPlane.position.y = 0.01;
317
- shadowPlane.receiveShadow = true;
318
- scene.add(shadowPlane);
319
-
320
- // Custom grid
321
- const gridHelper = new THREE.GridHelper(10, 20, 0x30363d, 0x21262d);
322
- gridHelper.position.y = 0.02;
323
- scene.add(gridHelper);
324
 
325
  // Constants
326
  const CENTER = new THREE.Vector3(0, 0.75, 0);
327
- const BASE_DISTANCE = 2.8;
328
- const AZIMUTH_RADIUS = 2.6;
329
- const ELEVATION_RADIUS = 2.0;
330
 
331
  // State
332
  let azimuthAngle = props.value?.azimuth || 0;
@@ -346,98 +297,51 @@ class LightingControl3D(gr.HTML):
346
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
347
  }
348
 
349
- // Create placeholder texture
350
  function createPlaceholderTexture() {
351
  const canvas = document.createElement('canvas');
352
  canvas.width = 256;
353
  canvas.height = 256;
354
  const ctx = canvas.getContext('2d');
355
-
356
- // Dark gradient background
357
- const gradient = ctx.createLinearGradient(0, 0, 256, 256);
358
- gradient.addColorStop(0, '#2d333b');
359
- gradient.addColorStop(1, '#22272e');
360
- ctx.fillStyle = gradient;
361
  ctx.fillRect(0, 0, 256, 256);
362
-
363
- // Border
364
- ctx.strokeStyle = '#444c56';
365
- ctx.lineWidth = 4;
366
- ctx.strokeRect(8, 8, 240, 240);
367
-
368
- // Icon - image placeholder
369
- ctx.strokeStyle = '#6e7681';
370
- ctx.lineWidth = 3;
371
  ctx.beginPath();
372
- ctx.roundRect(80, 90, 96, 76, 8);
373
- ctx.stroke();
374
-
375
- // Mountain icon
376
- ctx.fillStyle = '#6e7681';
377
- ctx.beginPath();
378
- ctx.moveTo(90, 150);
379
- ctx.lineTo(115, 115);
380
- ctx.lineTo(140, 145);
381
- ctx.lineTo(155, 125);
382
- ctx.lineTo(166, 150);
383
- ctx.closePath();
384
  ctx.fill();
385
-
386
- // Sun
387
  ctx.beginPath();
388
- ctx.arc(150, 110, 12, 0, Math.PI * 2);
 
389
  ctx.fill();
390
-
391
- // Text
392
- ctx.fillStyle = '#8b949e';
393
- ctx.font = '14px sans-serif';
394
- ctx.textAlign = 'center';
395
- ctx.fillText('Upload Image', 128, 195);
396
-
397
  return new THREE.CanvasTexture(canvas);
398
  }
399
 
400
  // Target image plane
401
  let currentTexture = createPlaceholderTexture();
402
- const planeMaterial = new THREE.MeshStandardMaterial({
403
- map: currentTexture,
404
- side: THREE.DoubleSide,
405
- roughness: 0.4,
406
- metalness: 0.1
407
- });
408
- let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), planeMaterial);
409
  targetPlane.position.copy(CENTER);
410
  targetPlane.receiveShadow = true;
411
- targetPlane.castShadow = true;
412
  scene.add(targetPlane);
413
 
414
- // Add frame around the image
415
- const frameGeometry = new THREE.BoxGeometry(1.5, 1.5, 0.05);
416
- const frameMaterial = new THREE.MeshStandardMaterial({
417
- color: 0x30363d,
418
- roughness: 0.3,
419
- metalness: 0.7
420
- });
421
- const frame = new THREE.Mesh(frameGeometry, frameMaterial);
422
- frame.position.set(CENTER.x, CENTER.y, -0.03);
423
- scene.add(frame);
424
-
425
  // Function to update texture from image URL
426
  function updateTextureFromUrl(url) {
427
  if (!url) {
 
428
  planeMaterial.map = createPlaceholderTexture();
429
  planeMaterial.needsUpdate = true;
 
430
  scene.remove(targetPlane);
431
- targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 1.4), planeMaterial);
432
  targetPlane.position.copy(CENTER);
433
  targetPlane.receiveShadow = true;
434
- targetPlane.castShadow = true;
435
  scene.add(targetPlane);
436
-
437
- // Reset frame
438
- frame.geometry.dispose();
439
- frame.geometry = new THREE.BoxGeometry(1.5, 1.5, 0.05);
440
- frame.position.set(CENTER.x, CENTER.y, -0.03);
441
  return;
442
  }
443
 
@@ -449,10 +353,11 @@ class LightingControl3D(gr.HTML):
449
  planeMaterial.map = texture;
450
  planeMaterial.needsUpdate = true;
451
 
 
452
  const img = texture.image;
453
  if (img && img.width && img.height) {
454
  const aspect = img.width / img.height;
455
- const maxSize = 1.6;
456
  let planeWidth, planeHeight;
457
  if (aspect > 1) {
458
  planeWidth = maxSize;
@@ -468,102 +373,54 @@ class LightingControl3D(gr.HTML):
468
  );
469
  targetPlane.position.copy(CENTER);
470
  targetPlane.receiveShadow = true;
471
- targetPlane.castShadow = true;
472
  scene.add(targetPlane);
473
-
474
- // Update frame
475
- frame.geometry.dispose();
476
- frame.geometry = new THREE.BoxGeometry(planeWidth + 0.1, planeHeight + 0.1, 0.05);
477
- frame.position.set(CENTER.x, CENTER.y, -0.03);
478
  }
479
  }, undefined, (err) => {
480
  console.error('Failed to load texture:', err);
481
  });
482
  }
483
 
 
484
  if (props.imageUrl) {
485
  updateTextureFromUrl(props.imageUrl);
486
  }
487
 
488
- // ==================== SOFTBOX LIGHT ====================
489
  const lightGroup = new THREE.Group();
490
-
491
- // Softbox frame (outer box) - RED
492
- const softboxWidth = 0.8;
493
- const softboxHeight = 0.8;
494
- const softboxDepth = 0.3;
495
-
496
- // Main softbox body
497
- const softboxGeometry = new THREE.BoxGeometry(softboxWidth, softboxHeight, softboxDepth);
498
- const softboxMaterial = new THREE.MeshStandardMaterial({
499
- color: 0xcc2222,
500
- roughness: 0.3,
501
- metalness: 0.6
502
  });
503
- const softboxBody = new THREE.Mesh(softboxGeometry, softboxMaterial);
504
- softboxBody.castShadow = true;
505
- lightGroup.add(softboxBody);
506
 
507
- // Softbox front diffuser (white glowing panel)
508
- const diffuserGeometry = new THREE.PlaneGeometry(softboxWidth - 0.08, softboxHeight - 0.08);
509
- const diffuserMaterial = new THREE.MeshStandardMaterial({
510
- color: 0xffffff,
511
  emissive: 0xffffff,
512
- emissiveIntensity: 2.5,
513
- roughness: 0.1,
514
- metalness: 0,
515
- transparent: true,
516
- opacity: 0.95
517
  });
518
- const diffuser = new THREE.Mesh(diffuserGeometry, diffuserMaterial);
519
- diffuser.position.z = softboxDepth / 2 + 0.01;
520
- lightGroup.add(diffuser);
521
 
522
- // Softbox edge trim - darker red
523
- const edgeGeometry = new THREE.BoxGeometry(softboxWidth + 0.04, softboxHeight + 0.04, 0.04);
524
- const edgeMaterial = new THREE.MeshStandardMaterial({
525
- color: 0x991111,
526
- roughness: 0.4,
527
- metalness: 0.7
528
- });
529
- const edge = new THREE.Mesh(edgeGeometry, edgeMaterial);
530
- edge.position.z = softboxDepth / 2;
531
- lightGroup.add(edge);
532
-
533
- // Mounting bracket
534
- const bracketGeometry = new THREE.CylinderGeometry(0.03, 0.03, 0.2, 8);
535
- const bracketMaterial = new THREE.MeshStandardMaterial({
536
- color: 0x333333,
537
- roughness: 0.5,
538
- metalness: 0.8
539
- });
540
- const bracket = new THREE.Mesh(bracketGeometry, bracketMaterial);
541
- bracket.rotation.x = Math.PI / 2;
542
- bracket.position.z = -softboxDepth / 2 - 0.1;
543
- lightGroup.add(bracket);
544
 
545
- // Ball joint
546
- const ballGeometry = new THREE.SphereGeometry(0.06, 16, 16);
547
- const ball = new THREE.Mesh(ballGeometry, bracketMaterial);
548
- ball.position.z = -softboxDepth / 2 - 0.2;
549
- lightGroup.add(ball);
550
-
551
- // Stand connector
552
- const standGeometry = new THREE.CylinderGeometry(0.025, 0.025, 0.15, 8);
553
- const stand = new THREE.Mesh(standGeometry, bracketMaterial);
554
- stand.position.z = -softboxDepth / 2 - 0.2;
555
- stand.position.y = -0.1;
556
- lightGroup.add(stand);
557
-
558
- // Actual spotlight for casting light - WHITE
559
- const spotLight = new THREE.SpotLight(0xffffff, 15, 12, Math.PI / 4, 0.8, 1);
560
- spotLight.position.set(0, 0, softboxDepth / 2);
561
  spotLight.castShadow = true;
562
- spotLight.shadow.mapSize.width = 2048;
563
- spotLight.shadow.mapSize.height = 2048;
564
  spotLight.shadow.camera.near = 0.5;
565
- spotLight.shadow.camera.far = 15;
566
- spotLight.shadow.bias = -0.0005;
567
  lightGroup.add(spotLight);
568
 
569
  const lightTarget = new THREE.Object3D();
@@ -571,150 +428,77 @@ class LightingControl3D(gr.HTML):
571
  scene.add(lightTarget);
572
  spotLight.target = lightTarget;
573
 
574
- // Add point light for softer fill
575
- const fillLight = new THREE.PointLight(0xffffff, 3, 8);
576
- fillLight.position.set(0, 0, softboxDepth / 2 + 0.2);
577
- lightGroup.add(fillLight);
578
-
579
  scene.add(lightGroup);
580
 
581
- // ==================== AZIMUTH RING (YELLOW) ====================
582
- const azimuthRingGeometry = new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.035, 16, 64);
583
- const azimuthRingMaterial = new THREE.MeshStandardMaterial({
584
- color: 0xffd700,
585
- emissive: 0xffd700,
586
- emissiveIntensity: 0.4,
587
- roughness: 0.3,
588
- metalness: 0.7
589
- });
590
- const azimuthRing = new THREE.Mesh(azimuthRingGeometry, azimuthRingMaterial);
591
  azimuthRing.rotation.x = Math.PI / 2;
592
  azimuthRing.position.y = 0.05;
593
  scene.add(azimuthRing);
594
 
595
- // Azimuth handle - YELLOW with glow effect
596
- const azimuthHandleGroup = new THREE.Group();
597
-
598
- const azimuthHandleCore = new THREE.Mesh(
599
- new THREE.SphereGeometry(0.16, 24, 24),
600
- new THREE.MeshStandardMaterial({
601
- color: 0xffd700,
602
- emissive: 0xffd700,
603
- emissiveIntensity: 0.6,
604
- roughness: 0.2,
605
- metalness: 0.8
606
- })
607
- );
608
- azimuthHandleGroup.add(azimuthHandleCore);
609
-
610
- // Outer glow ring
611
- const azimuthGlow = new THREE.Mesh(
612
- new THREE.TorusGeometry(0.2, 0.02, 16, 32),
613
- new THREE.MeshStandardMaterial({
614
- color: 0xffee00,
615
- emissive: 0xffee00,
616
- emissiveIntensity: 1,
617
- transparent: true,
618
- opacity: 0.7
619
- })
620
  );
621
- azimuthGlow.rotation.x = Math.PI / 2;
622
- azimuthHandleGroup.add(azimuthGlow);
623
 
624
- azimuthHandleGroup.userData.type = 'azimuth';
625
- scene.add(azimuthHandleGroup);
626
-
627
- // ==================== ELEVATION ARC (BLUE) ====================
628
  const arcPoints = [];
629
- for (let i = 0; i <= 48; i++) {
630
- const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 48));
631
- arcPoints.push(new THREE.Vector3(
632
- -1.0,
633
- ELEVATION_RADIUS * Math.sin(angle) + CENTER.y,
634
- ELEVATION_RADIUS * Math.cos(angle)
635
- ));
636
  }
637
  const arcCurve = new THREE.CatmullRomCurve3(arcPoints);
638
  const elevationArc = new THREE.Mesh(
639
- new THREE.TubeGeometry(arcCurve, 48, 0.035, 8, false),
640
- new THREE.MeshStandardMaterial({
641
- color: 0x0088ff,
642
- emissive: 0x0088ff,
643
- emissiveIntensity: 0.4,
644
- roughness: 0.3,
645
- metalness: 0.7
646
- })
647
  );
648
  scene.add(elevationArc);
649
 
650
- // Elevation handle - BLUE with glow effect
651
- const elevationHandleGroup = new THREE.Group();
652
-
653
- const elevationHandleCore = new THREE.Mesh(
654
- new THREE.SphereGeometry(0.16, 24, 24),
655
- new THREE.MeshStandardMaterial({
656
- color: 0x0088ff,
657
- emissive: 0x0088ff,
658
- emissiveIntensity: 0.6,
659
- roughness: 0.2,
660
- metalness: 0.8
661
- })
662
  );
663
- elevationHandleGroup.add(elevationHandleCore);
664
-
665
- // Outer glow ring
666
- const elevationGlow = new THREE.Mesh(
667
- new THREE.TorusGeometry(0.2, 0.02, 16, 32),
668
- new THREE.MeshStandardMaterial({
669
- color: 0x00aaff,
670
- emissive: 0x00aaff,
671
- emissiveIntensity: 1,
672
- transparent: true,
673
- opacity: 0.7
674
- })
675
- );
676
- elevationGlow.rotation.x = Math.PI / 2;
677
- elevationHandleGroup.add(elevationGlow);
678
-
679
- elevationHandleGroup.userData.type = 'elevation';
680
- scene.add(elevationHandleGroup);
 
 
 
 
 
 
 
 
681
 
682
- // Add direction markers on azimuth ring
683
- const markerPositions = [0, 90, 180, 270];
684
- const markerLabels = ['F', 'R', 'B', 'L'];
685
- markerPositions.forEach((deg, idx) => {
686
- const rad = THREE.MathUtils.degToRad(deg);
687
- const marker = new THREE.Mesh(
688
- new THREE.CylinderGeometry(0.04, 0.04, 0.08, 8),
689
- new THREE.MeshStandardMaterial({
690
- color: 0x666666,
691
- roughness: 0.5,
692
- metalness: 0.5
693
- })
694
- );
695
- marker.position.set(
696
- AZIMUTH_RADIUS * Math.sin(rad),
697
- 0.05,
698
- AZIMUTH_RADIUS * Math.cos(rad)
699
- );
700
- scene.add(marker);
701
- });
702
-
703
- // Refresh button click handler
704
  refreshBtn.addEventListener('click', () => {
705
  azimuthAngle = 0;
706
  elevationAngle = 0;
707
-
708
- // Animate reset
709
- const startTime = Date.now();
710
- function animateReset() {
711
- const t = Math.min((Date.now() - startTime) / 300, 1);
712
- const ease = 1 - Math.pow(1 - t, 3);
713
- updatePositions();
714
- if (t < 1) requestAnimationFrame(animateReset);
715
- else updatePropsAndTrigger();
716
- }
717
- animateReset();
718
  });
719
 
720
  function updatePositions() {
@@ -729,19 +513,8 @@ class LightingControl3D(gr.HTML):
729
  lightGroup.position.set(lightX, lightY, lightZ);
730
  lightGroup.lookAt(CENTER);
731
 
732
- // Update azimuth handle
733
- azimuthHandleGroup.position.set(
734
- AZIMUTH_RADIUS * Math.sin(azRad),
735
- 0.05,
736
- AZIMUTH_RADIUS * Math.cos(azRad)
737
- );
738
-
739
- // Update elevation handle
740
- elevationHandleGroup.position.set(
741
- -1.0,
742
- ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y,
743
- ELEVATION_RADIUS * Math.cos(elRad)
744
- );
745
 
746
  // Update prompt
747
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
@@ -752,7 +525,7 @@ class LightingControl3D(gr.HTML):
752
  } else {
753
  prompt += ' the ' + azimuthNames[azSnap];
754
  }
755
- promptOverlay.textContent = '💡 ' + prompt;
756
  }
757
 
758
  function updatePropsAndTrigger() {
@@ -768,33 +541,25 @@ class LightingControl3D(gr.HTML):
768
  const mouse = new THREE.Vector2();
769
  let isDragging = false;
770
  let dragTarget = null;
 
771
  const intersection = new THREE.Vector3();
772
 
773
  const canvas = renderer.domElement;
774
 
775
- function getHandleFromGroup(group) {
776
- return group;
777
- }
778
-
779
  canvas.addEventListener('mousedown', (e) => {
780
  const rect = canvas.getBoundingClientRect();
781
  mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
782
  mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
783
 
784
  raycaster.setFromCamera(mouse, camera);
785
- const intersects = raycaster.intersectObjects([...azimuthHandleGroup.children, ...elevationHandleGroup.children]);
786
 
787
  if (intersects.length > 0) {
788
  isDragging = true;
789
- dragTarget = intersects[0].object.parent;
790
-
791
- // Highlight effect
792
- dragTarget.children.forEach(child => {
793
- if (child.material && child.material.emissiveIntensity !== undefined) {
794
- child.material.emissiveIntensity = 1.2;
795
- }
796
- });
797
  dragTarget.scale.setScalar(1.3);
 
798
  canvas.style.cursor = 'grabbing';
799
  }
800
  });
@@ -814,43 +579,24 @@ class LightingControl3D(gr.HTML):
814
  if (azimuthAngle < 0) azimuthAngle += 360;
815
  }
816
  } else if (dragTarget.userData.type === 'elevation') {
817
- const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 1.0);
818
  if (raycaster.ray.intersectPlane(plane, intersection)) {
819
  const relY = intersection.y - CENTER.y;
820
  const relZ = intersection.z;
821
- elevationAngle = THREE.MathUtils.clamp(
822
- THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)),
823
- -90,
824
- 90
825
- );
826
  }
827
  }
828
  updatePositions();
829
  } else {
830
  raycaster.setFromCamera(mouse, camera);
831
- const intersects = raycaster.intersectObjects([
832
- ...azimuthHandleGroup.children,
833
- ...elevationHandleGroup.children
834
- ]);
835
-
836
- // Reset all handles
837
- [azimuthHandleGroup, elevationHandleGroup].forEach(group => {
838
- group.children.forEach(child => {
839
- if (child.material && child.material.emissiveIntensity !== undefined) {
840
- child.material.emissiveIntensity = child.geometry.type === 'TorusGeometry' ? 1 : 0.6;
841
- }
842
- });
843
- group.scale.setScalar(1);
844
  });
845
-
846
  if (intersects.length > 0) {
847
- const hoveredGroup = intersects[0].object.parent;
848
- hoveredGroup.children.forEach(child => {
849
- if (child.material && child.material.emissiveIntensity !== undefined) {
850
- child.material.emissiveIntensity = 1.0;
851
- }
852
- });
853
- hoveredGroup.scale.setScalar(1.15);
854
  canvas.style.cursor = 'grab';
855
  } else {
856
  canvas.style.cursor = 'default';
@@ -860,15 +606,10 @@ class LightingControl3D(gr.HTML):
860
 
861
  const onMouseUp = () => {
862
  if (dragTarget) {
863
- // Reset highlight
864
- dragTarget.children.forEach(child => {
865
- if (child.material && child.material.emissiveIntensity !== undefined) {
866
- child.material.emissiveIntensity = child.geometry.type === 'TorusGeometry' ? 1 : 0.6;
867
- }
868
- });
869
  dragTarget.scale.setScalar(1);
870
 
871
- // Snap animation
872
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
873
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
874
 
@@ -901,8 +642,7 @@ class LightingControl3D(gr.HTML):
901
 
902
  canvas.addEventListener('mouseup', onMouseUp);
903
  canvas.addEventListener('mouseleave', onMouseUp);
904
-
905
- // Touch support
906
  canvas.addEventListener('touchstart', (e) => {
907
  e.preventDefault();
908
  const touch = e.touches[0];
@@ -911,20 +651,14 @@ class LightingControl3D(gr.HTML):
911
  mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
912
 
913
  raycaster.setFromCamera(mouse, camera);
914
- const intersects = raycaster.intersectObjects([
915
- ...azimuthHandleGroup.children,
916
- ...elevationHandleGroup.children
917
- ]);
918
 
919
  if (intersects.length > 0) {
920
  isDragging = true;
921
- dragTarget = intersects[0].object.parent;
922
- dragTarget.children.forEach(child => {
923
- if (child.material && child.material.emissiveIntensity !== undefined) {
924
- child.material.emissiveIntensity = 1.2;
925
- }
926
- });
927
  dragTarget.scale.setScalar(1.3);
 
928
  }
929
  }, { passive: false });
930
 
@@ -945,15 +679,11 @@ class LightingControl3D(gr.HTML):
945
  if (azimuthAngle < 0) azimuthAngle += 360;
946
  }
947
  } else if (dragTarget.userData.type === 'elevation') {
948
- const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 1.0);
949
  if (raycaster.ray.intersectPlane(plane, intersection)) {
950
  const relY = intersection.y - CENTER.y;
951
  const relZ = intersection.z;
952
- elevationAngle = THREE.MathUtils.clamp(
953
- THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)),
954
- -90,
955
- 90
956
- );
957
  }
958
  }
959
  updatePositions();
@@ -973,17 +703,9 @@ class LightingControl3D(gr.HTML):
973
  // Initial update
974
  updatePositions();
975
 
976
- // Animation for subtle effects
977
- let time = 0;
978
  function render() {
979
  requestAnimationFrame(render);
980
- time += 0.016;
981
-
982
- // Subtle pulse on glow rings
983
- const pulse = 0.7 + Math.sin(time * 2) * 0.1;
984
- azimuthGlow.material.opacity = pulse;
985
- elevationGlow.material.opacity = pulse;
986
-
987
  renderer.render(scene, camera);
988
  }
989
  render();
@@ -995,7 +717,7 @@ class LightingControl3D(gr.HTML):
995
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
996
  }).observe(wrapper);
997
 
998
- // Store update functions
999
  wrapper._updateFromProps = (newVal) => {
1000
  if (newVal && typeof newVal === 'object') {
1001
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
@@ -1006,14 +728,16 @@ class LightingControl3D(gr.HTML):
1006
 
1007
  wrapper._updateTexture = updateTextureFromUrl;
1008
 
1009
- // Watch for prop changes
1010
  let lastImageUrl = props.imageUrl;
1011
  let lastValue = JSON.stringify(props.value);
1012
  setInterval(() => {
 
1013
  if (props.imageUrl !== lastImageUrl) {
1014
  lastImageUrl = props.imageUrl;
1015
  updateTextureFromUrl(props.imageUrl);
1016
  }
 
1017
  const currentValue = JSON.stringify(props.value);
1018
  if (currentValue !== lastValue) {
1019
  lastValue = currentValue;
@@ -1045,7 +769,6 @@ css = '''
1045
  .slider-row { display: flex; gap: 10px; align-items: center; }
1046
  #main-title h1 {font-size: 2.4em !important;}
1047
  '''
1048
-
1049
  with gr.Blocks(css=css) as demo:
1050
  gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
1051
  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).")
 
88
  pipe = QwenImageEditPlusPipeline.from_pretrained(
89
  "Qwen/Qwen-Image-Edit-2511",
90
  transformer=QwenImageTransformer2DModel.from_pretrained(
91
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
92
  torch_dtype=dtype,
93
  device_map='cuda'
94
  ),
 
225
  value = {"azimuth": 0, "elevation": 0}
226
 
227
  html_template = """
228
+ <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: #1a1a1a; border-radius: 12px; overflow: hidden;">
229
+ <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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </div>
231
  """
232
 
 
234
  (() => {
235
  const wrapper = element.querySelector('#lighting-control-wrapper');
236
  const promptOverlay = element.querySelector('#prompt-overlay');
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  // Wait for THREE to load
239
  const initScene = () => {
 
244
 
245
  // Scene setup
246
  const scene = new THREE.Scene();
247
+ scene.background = new THREE.Color(0x1a1a1a);
 
 
 
248
 
249
  const camera = new THREE.PerspectiveCamera(50, wrapper.clientWidth / wrapper.clientHeight, 0.1, 1000);
250
  camera.position.set(4.5, 3, 4.5);
 
255
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
256
  renderer.shadowMap.enabled = true;
257
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
258
+ wrapper.insertBefore(renderer.domElement, promptOverlay);
 
 
259
 
260
+ // Lighting for the scene
261
+ scene.add(new THREE.AmbientLight(0xffffff, 0.1));
262
 
263
+ // Ground plane for shadows
264
+ const ground = new THREE.Mesh(
265
+ new THREE.PlaneGeometry(10, 10),
266
+ new THREE.ShadowMaterial({ opacity: 0.3 })
267
+ );
 
 
 
268
  ground.rotation.x = -Math.PI / 2;
269
  ground.position.y = 0;
270
  ground.receiveShadow = true;
271
  scene.add(ground);
272
 
273
+ // Grid
274
+ scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  // Constants
277
  const CENTER = new THREE.Vector3(0, 0.75, 0);
278
+ const BASE_DISTANCE = 2.5;
279
+ const AZIMUTH_RADIUS = 2.4;
280
+ const ELEVATION_RADIUS = 1.8;
281
 
282
  // State
283
  let azimuthAngle = props.value?.azimuth || 0;
 
297
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
298
  }
299
 
300
+ // Create placeholder texture (smiley face)
301
  function createPlaceholderTexture() {
302
  const canvas = document.createElement('canvas');
303
  canvas.width = 256;
304
  canvas.height = 256;
305
  const ctx = canvas.getContext('2d');
306
+ ctx.fillStyle = '#3a3a4a';
 
 
 
 
 
307
  ctx.fillRect(0, 0, 256, 256);
308
+ ctx.fillStyle = '#ffcc99';
 
 
 
 
 
 
 
 
309
  ctx.beginPath();
310
+ ctx.arc(128, 128, 80, 0, Math.PI * 2);
 
 
 
 
 
 
 
 
 
 
 
311
  ctx.fill();
312
+ ctx.fillStyle = '#333';
 
313
  ctx.beginPath();
314
+ ctx.arc(100, 110, 10, 0, Math.PI * 2);
315
+ ctx.arc(156, 110, 10, 0, Math.PI * 2);
316
  ctx.fill();
317
+ ctx.strokeStyle = '#333';
318
+ ctx.lineWidth = 3;
319
+ ctx.beginPath();
320
+ ctx.arc(128, 130, 35, 0.2, Math.PI - 0.2);
321
+ ctx.stroke();
 
 
322
  return new THREE.CanvasTexture(canvas);
323
  }
324
 
325
  // Target image plane
326
  let currentTexture = createPlaceholderTexture();
327
+ const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
328
+ let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
 
 
 
 
 
329
  targetPlane.position.copy(CENTER);
330
  targetPlane.receiveShadow = true;
 
331
  scene.add(targetPlane);
332
 
 
 
 
 
 
 
 
 
 
 
 
333
  // Function to update texture from image URL
334
  function updateTextureFromUrl(url) {
335
  if (!url) {
336
+ // Reset to placeholder
337
  planeMaterial.map = createPlaceholderTexture();
338
  planeMaterial.needsUpdate = true;
339
+ // Reset plane to square
340
  scene.remove(targetPlane);
341
+ targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
342
  targetPlane.position.copy(CENTER);
343
  targetPlane.receiveShadow = true;
 
344
  scene.add(targetPlane);
 
 
 
 
 
345
  return;
346
  }
347
 
 
353
  planeMaterial.map = texture;
354
  planeMaterial.needsUpdate = true;
355
 
356
+ // Adjust plane aspect ratio to match image
357
  const img = texture.image;
358
  if (img && img.width && img.height) {
359
  const aspect = img.width / img.height;
360
+ const maxSize = 1.5;
361
  let planeWidth, planeHeight;
362
  if (aspect > 1) {
363
  planeWidth = maxSize;
 
373
  );
374
  targetPlane.position.copy(CENTER);
375
  targetPlane.receiveShadow = true;
 
376
  scene.add(targetPlane);
 
 
 
 
 
377
  }
378
  }, undefined, (err) => {
379
  console.error('Failed to load texture:', err);
380
  });
381
  }
382
 
383
+ // Check for initial imageUrl
384
  if (props.imageUrl) {
385
  updateTextureFromUrl(props.imageUrl);
386
  }
387
 
388
+ // --- NEW LIGHT MODEL: SOFTBOX ---
389
  const lightGroup = new THREE.Group();
390
+
391
+ // 1. Softbox Housing (Red)
392
+ const housingGeo = new THREE.BoxGeometry(0.6, 0.6, 0.4);
393
+ const housingMat = new THREE.MeshStandardMaterial({
394
+ color: 0xff0000, // RED HOUSING
395
+ roughness: 0.5,
396
+ metalness: 0.1
 
 
 
 
 
397
  });
398
+ const housing = new THREE.Mesh(housingGeo, housingMat);
 
 
399
 
400
+ // 2. Diffuser Panel (White, Emissive)
401
+ const diffuserGeo = new THREE.PlaneGeometry(0.55, 0.55);
402
+ const diffuserMat = new THREE.MeshStandardMaterial({
403
+ color: 0xffffff, // WHITE LIGHT
404
  emissive: 0xffffff,
405
+ emissiveIntensity: 2.0,
406
+ roughness: 0.2
 
 
 
407
  });
408
+ const diffuser = new THREE.Mesh(diffuserGeo, diffuserMat);
409
+ diffuser.position.z = 0.201; // Slightly in front of the housing
 
410
 
411
+ // Assemble the softbox
412
+ housing.add(diffuser);
413
+ lightGroup.add(housing);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ // Actual Light Source
416
+ const spotLight = new THREE.SpotLight(0xffffff, 10, 10, Math.PI / 3, 1, 1);
417
+ spotLight.position.set(0, 0, -0.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  spotLight.castShadow = true;
419
+ spotLight.shadow.mapSize.width = 1024;
420
+ spotLight.shadow.mapSize.height = 1024;
421
  spotLight.shadow.camera.near = 0.5;
422
+ spotLight.shadow.camera.far = 500;
423
+ spotLight.shadow.bias = -0.005;
424
  lightGroup.add(spotLight);
425
 
426
  const lightTarget = new THREE.Object3D();
 
428
  scene.add(lightTarget);
429
  spotLight.target = lightTarget;
430
 
 
 
 
 
 
431
  scene.add(lightGroup);
432
 
433
+ // --- CONTROLS: COLORS UPDATED ---
434
+
435
+ // Azimuth ring (YELLOW)
436
+ const azimuthRing = new THREE.Mesh(
437
+ new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
438
+ new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
439
+ );
 
 
 
440
  azimuthRing.rotation.x = Math.PI / 2;
441
  azimuthRing.position.y = 0.05;
442
  scene.add(azimuthRing);
443
 
444
+ // Azimuth Handle (YELLOW)
445
+ const azimuthHandle = new THREE.Mesh(
446
+ new THREE.SphereGeometry(0.18, 16, 16),
447
+ new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  );
449
+ azimuthHandle.userData.type = 'azimuth';
450
+ scene.add(azimuthHandle);
451
 
452
+ // Elevation arc (BLUE)
 
 
 
453
  const arcPoints = [];
454
+ for (let i = 0; i <= 32; i++) {
455
+ const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
456
+ arcPoints.push(new THREE.Vector3(-0.8, ELEVATION_RADIUS * Math.sin(angle) + CENTER.y, ELEVATION_RADIUS * Math.cos(angle)));
 
 
 
 
457
  }
458
  const arcCurve = new THREE.CatmullRomCurve3(arcPoints);
459
  const elevationArc = new THREE.Mesh(
460
+ new THREE.TubeGeometry(arcCurve, 32, 0.04, 8, false),
461
+ new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.3 })
 
 
 
 
 
 
462
  );
463
  scene.add(elevationArc);
464
 
465
+ // Elevation Handle (BLUE)
466
+ const elevationHandle = new THREE.Mesh(
467
+ new THREE.SphereGeometry(0.18, 16, 16),
468
+ new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
 
 
 
 
 
 
 
 
469
  );
470
+ elevationHandle.userData.type = 'elevation';
471
+ scene.add(elevationHandle);
472
+
473
+ // --- REFRESH BUTTON (Redesigned) ---
474
+ const refreshBtn = document.createElement('button');
475
+ refreshBtn.innerHTML = 'Reset View';
476
+ refreshBtn.style.position = 'absolute';
477
+ refreshBtn.style.top = '15px';
478
+ refreshBtn.style.right = '15px';
479
+ refreshBtn.style.background = '#e63e00'; // Theme primary color
480
+ refreshBtn.style.color = '#fff';
481
+ refreshBtn.style.border = 'none';
482
+ refreshBtn.style.padding = '8px 16px';
483
+ refreshBtn.style.borderRadius = '6px';
484
+ refreshBtn.style.cursor = 'pointer';
485
+ refreshBtn.style.zIndex = '10';
486
+ refreshBtn.style.fontSize = '14px';
487
+ refreshBtn.style.fontWeight = '600';
488
+ refreshBtn.style.fontFamily = 'system-ui, sans-serif';
489
+ refreshBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
490
+ refreshBtn.style.transition = 'background 0.2s';
491
+
492
+ refreshBtn.onmouseover = () => refreshBtn.style.background = '#ff5722';
493
+ refreshBtn.onmouseout = () => refreshBtn.style.background = '#e63e00';
494
+
495
+ wrapper.appendChild(refreshBtn);
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  refreshBtn.addEventListener('click', () => {
498
  azimuthAngle = 0;
499
  elevationAngle = 0;
500
+ updatePositions();
501
+ updatePropsAndTrigger();
 
 
 
 
 
 
 
 
 
502
  });
503
 
504
  function updatePositions() {
 
513
  lightGroup.position.set(lightX, lightY, lightZ);
514
  lightGroup.lookAt(CENTER);
515
 
516
+ azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
517
+ elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
 
 
 
 
 
 
 
 
 
 
 
518
 
519
  // Update prompt
520
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
 
525
  } else {
526
  prompt += ' the ' + azimuthNames[azSnap];
527
  }
528
+ promptOverlay.textContent = prompt;
529
  }
530
 
531
  function updatePropsAndTrigger() {
 
541
  const mouse = new THREE.Vector2();
542
  let isDragging = false;
543
  let dragTarget = null;
544
+ let dragStartMouse = new THREE.Vector2();
545
  const intersection = new THREE.Vector3();
546
 
547
  const canvas = renderer.domElement;
548
 
 
 
 
 
549
  canvas.addEventListener('mousedown', (e) => {
550
  const rect = canvas.getBoundingClientRect();
551
  mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
552
  mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
553
 
554
  raycaster.setFromCamera(mouse, camera);
555
+ const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
556
 
557
  if (intersects.length > 0) {
558
  isDragging = true;
559
+ dragTarget = intersects[0].object;
560
+ dragTarget.material.emissiveIntensity = 1.0;
 
 
 
 
 
 
561
  dragTarget.scale.setScalar(1.3);
562
+ dragStartMouse.copy(mouse);
563
  canvas.style.cursor = 'grabbing';
564
  }
565
  });
 
579
  if (azimuthAngle < 0) azimuthAngle += 360;
580
  }
581
  } else if (dragTarget.userData.type === 'elevation') {
582
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -0.8);
583
  if (raycaster.ray.intersectPlane(plane, intersection)) {
584
  const relY = intersection.y - CENTER.y;
585
  const relZ = intersection.z;
586
+ elevationAngle = THREE.MathUtils.clamp(THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)), -90, 90);
 
 
 
 
587
  }
588
  }
589
  updatePositions();
590
  } else {
591
  raycaster.setFromCamera(mouse, camera);
592
+ const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
593
+ [azimuthHandle, elevationHandle].forEach(h => {
594
+ h.material.emissiveIntensity = 0.5;
595
+ h.scale.setScalar(1);
 
 
 
 
 
 
 
 
 
596
  });
 
597
  if (intersects.length > 0) {
598
+ intersects[0].object.material.emissiveIntensity = 0.8;
599
+ intersects[0].object.scale.setScalar(1.1);
 
 
 
 
 
600
  canvas.style.cursor = 'grab';
601
  } else {
602
  canvas.style.cursor = 'default';
 
606
 
607
  const onMouseUp = () => {
608
  if (dragTarget) {
609
+ dragTarget.material.emissiveIntensity = 0.5;
 
 
 
 
 
610
  dragTarget.scale.setScalar(1);
611
 
612
+ // Snap and animate
613
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
614
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
615
 
 
642
 
643
  canvas.addEventListener('mouseup', onMouseUp);
644
  canvas.addEventListener('mouseleave', onMouseUp);
645
+ // Touch support for mobile
 
646
  canvas.addEventListener('touchstart', (e) => {
647
  e.preventDefault();
648
  const touch = e.touches[0];
 
651
  mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
652
 
653
  raycaster.setFromCamera(mouse, camera);
654
+ const intersects = raycaster.intersectObjects([azimuthHandle, elevationHandle]);
 
 
 
655
 
656
  if (intersects.length > 0) {
657
  isDragging = true;
658
+ dragTarget = intersects[0].object;
659
+ dragTarget.material.emissiveIntensity = 1.0;
 
 
 
 
660
  dragTarget.scale.setScalar(1.3);
661
+ dragStartMouse.copy(mouse);
662
  }
663
  }, { passive: false });
664
 
 
679
  if (azimuthAngle < 0) azimuthAngle += 360;
680
  }
681
  } else if (dragTarget.userData.type === 'elevation') {
682
+ const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -0.8);
683
  if (raycaster.ray.intersectPlane(plane, intersection)) {
684
  const relY = intersection.y - CENTER.y;
685
  const relZ = intersection.z;
686
+ elevationAngle = THREE.MathUtils.clamp(THREE.MathUtils.radToDeg(Math.atan2(relY, relZ)), -90, 90);
 
 
 
 
687
  }
688
  }
689
  updatePositions();
 
703
  // Initial update
704
  updatePositions();
705
 
706
+ // Render loop
 
707
  function render() {
708
  requestAnimationFrame(render);
 
 
 
 
 
 
 
709
  renderer.render(scene, camera);
710
  }
711
  render();
 
717
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
718
  }).observe(wrapper);
719
 
720
+ // Store update functions for external calls
721
  wrapper._updateFromProps = (newVal) => {
722
  if (newVal && typeof newVal === 'object') {
723
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
 
728
 
729
  wrapper._updateTexture = updateTextureFromUrl;
730
 
731
+ // Watch for prop changes (imageUrl and value)
732
  let lastImageUrl = props.imageUrl;
733
  let lastValue = JSON.stringify(props.value);
734
  setInterval(() => {
735
+ // Check imageUrl changes
736
  if (props.imageUrl !== lastImageUrl) {
737
  lastImageUrl = props.imageUrl;
738
  updateTextureFromUrl(props.imageUrl);
739
  }
740
+ // Check value changes (from sliders)
741
  const currentValue = JSON.stringify(props.value);
742
  if (currentValue !== lastValue) {
743
  lastValue = currentValue;
 
769
  .slider-row { display: flex; gap: 10px; align-items: center; }
770
  #main-title h1 {font-size: 2.4em !important;}
771
  '''
 
772
  with gr.Blocks(css=css) as demo:
773
  gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
774
  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).")