prithivMLmods commited on
Commit
dd88c02
·
verified ·
1 Parent(s): 401300c

update app

Browse files
Files changed (1) hide show
  1. app.py +83 -152
app.py CHANGED
@@ -234,12 +234,14 @@ class LightingControl3D(gr.HTML):
234
  const wrapper = element.querySelector('#lighting-control-wrapper');
235
  const promptOverlay = element.querySelector('#prompt-overlay');
236
 
 
237
  const initScene = () => {
238
  if (typeof THREE === 'undefined') {
239
  setTimeout(initScene, 100);
240
  return;
241
  }
242
 
 
243
  const scene = new THREE.Scene();
244
  scene.background = new THREE.Color(0x1a1a1a);
245
 
@@ -254,8 +256,10 @@ class LightingControl3D(gr.HTML):
254
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
255
  wrapper.insertBefore(renderer.domElement, promptOverlay);
256
 
 
257
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
258
 
 
259
  const ground = new THREE.Mesh(
260
  new THREE.PlaneGeometry(10, 10),
261
  new THREE.ShadowMaterial({ opacity: 0.3 })
@@ -265,16 +269,20 @@ class LightingControl3D(gr.HTML):
265
  ground.receiveShadow = true;
266
  scene.add(ground);
267
 
 
268
  scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
269
 
 
270
  const CENTER = new THREE.Vector3(0, 0.75, 0);
271
  const BASE_DISTANCE = 2.5;
272
  const AZIMUTH_RADIUS = 2.4;
273
  const ELEVATION_RADIUS = 1.8;
274
 
 
275
  let azimuthAngle = props.value?.azimuth || 0;
276
  let elevationAngle = props.value?.elevation || 0;
277
 
 
278
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
279
  const elevationSteps = [-90, 0, 90];
280
  const azimuthNames = {
@@ -288,6 +296,7 @@ class LightingControl3D(gr.HTML):
288
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
289
  }
290
 
 
291
  function createPlaceholderTexture() {
292
  const canvas = document.createElement('canvas');
293
  canvas.width = 256;
@@ -312,6 +321,7 @@ class LightingControl3D(gr.HTML):
312
  return new THREE.CanvasTexture(canvas);
313
  }
314
 
 
315
  let currentTexture = createPlaceholderTexture();
316
  const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
317
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
@@ -319,10 +329,13 @@ class LightingControl3D(gr.HTML):
319
  targetPlane.receiveShadow = true;
320
  scene.add(targetPlane);
321
 
 
322
  function updateTextureFromUrl(url) {
323
  if (!url) {
 
324
  planeMaterial.map = createPlaceholderTexture();
325
  planeMaterial.needsUpdate = true;
 
326
  scene.remove(targetPlane);
327
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
328
  targetPlane.position.copy(CENTER);
@@ -339,6 +352,7 @@ class LightingControl3D(gr.HTML):
339
  planeMaterial.map = texture;
340
  planeMaterial.needsUpdate = true;
341
 
 
342
  const img = texture.image;
343
  if (img && img.width && img.height) {
344
  const aspect = img.width / img.height;
@@ -365,161 +379,63 @@ class LightingControl3D(gr.HTML):
365
  });
366
  }
367
 
 
368
  if (props.imageUrl) {
369
  updateTextureFromUrl(props.imageUrl);
370
  }
371
 
372
- // Square Studio Light Design
373
  const lightGroup = new THREE.Group();
374
-
375
- // Main light body - dark metallic frame
376
- const frameGeo = new THREE.BoxGeometry(0.65, 0.65, 0.12);
377
- const frameMat = new THREE.MeshStandardMaterial({
378
- color: 0x1a1a1a,
379
- roughness: 0.2,
380
- metalness: 0.9
381
- });
382
- const frame = new THREE.Mesh(frameGeo, frameMat);
383
- lightGroup.add(frame);
384
-
385
- // Silver border trim
386
- const borderGeo = new THREE.BoxGeometry(0.68, 0.68, 0.08);
387
- const borderMat = new THREE.MeshStandardMaterial({
388
- color: 0x888888,
389
  roughness: 0.3,
390
- metalness: 0.8
391
- });
392
- const border = new THREE.Mesh(borderGeo, borderMat);
393
- border.position.z = 0.02;
394
- lightGroup.add(border);
395
-
396
- // LED Panel surface with grid texture
397
- const panelCanvas = document.createElement('canvas');
398
- panelCanvas.width = 128;
399
- panelCanvas.height = 128;
400
- const panelCtx = panelCanvas.getContext('2d');
401
- panelCtx.fillStyle = '#ffffff';
402
- panelCtx.fillRect(0, 0, 128, 128);
403
- panelCtx.strokeStyle = '#f0f0f0';
404
- panelCtx.lineWidth = 1;
405
- for (let i = 0; i <= 8; i++) {
406
- panelCtx.beginPath();
407
- panelCtx.moveTo(i * 16, 0);
408
- panelCtx.lineTo(i * 16, 128);
409
- panelCtx.stroke();
410
- panelCtx.beginPath();
411
- panelCtx.moveTo(0, i * 16);
412
- panelCtx.lineTo(128, i * 16);
413
- panelCtx.stroke();
414
- }
415
- const panelTexture = new THREE.CanvasTexture(panelCanvas);
416
-
417
- const panelGeo = new THREE.PlaneGeometry(0.58, 0.58);
418
- const panelMat = new THREE.MeshStandardMaterial({
419
- map: panelTexture,
420
- color: 0xffffff,
421
- emissive: 0xffffee,
422
- emissiveIntensity: 2.0,
423
- roughness: 0.1
424
  });
425
- const panel = new THREE.Mesh(panelGeo, panelMat);
426
- panel.position.z = 0.061;
427
- lightGroup.add(panel);
428
-
429
- // Corner accent lights
430
- const accentPositions = [
431
- [-0.28, -0.28], [0.28, -0.28], [0.28, 0.28], [-0.28, 0.28]
432
- ];
433
- accentPositions.forEach(([x, y]) => {
434
- const accentGeo = new THREE.CircleGeometry(0.03, 16);
435
- const accentMat = new THREE.MeshBasicMaterial({
436
- color: 0x00ff88,
437
- transparent: true,
438
- opacity: 0.8
439
- });
440
- const accent = new THREE.Mesh(accentGeo, accentMat);
441
- accent.position.set(x, y, 0.062);
442
- lightGroup.add(accent);
443
  });
 
 
 
444
 
445
- // Natural Light Rays - Lines emanating from panel
446
- const rayGroup = new THREE.Group();
 
 
 
447
 
448
- // Create rays from grid points on the panel
449
- const rayStartPoints = [];
450
- const gridSize = 3;
451
- for (let i = -gridSize; i <= gridSize; i++) {
452
- for (let j = -gridSize; j <= gridSize; j++) {
453
- rayStartPoints.push([i * 0.08, j * 0.08]);
454
- }
455
- }
456
 
457
- // Add corner rays
458
- const cornerSpread = 0.25;
459
- rayStartPoints.push([-cornerSpread, -cornerSpread]);
460
- rayStartPoints.push([cornerSpread, -cornerSpread]);
461
- rayStartPoints.push([cornerSpread, cornerSpread]);
462
- rayStartPoints.push([-cornerSpread, cornerSpread]);
463
-
464
- rayStartPoints.forEach(([x, y], index) => {
465
- const rayLength = 1.8 + Math.random() * 0.4;
466
- const spreadFactor = 0.15;
467
-
468
- // Calculate end point with natural spread
469
- const endX = x * (1 + rayLength * spreadFactor);
470
- const endY = y * (1 + rayLength * spreadFactor);
471
- const endZ = rayLength;
472
-
473
- // Create gradient effect using multiple segments
474
- const segments = 8;
475
- for (let s = 0; s < segments; s++) {
476
- const t1 = s / segments;
477
- const t2 = (s + 1) / segments;
478
-
479
- const x1 = x + (endX - x) * t1;
480
- const y1 = y + (endY - y) * t1;
481
- const z1 = 0.07 + endZ * t1;
482
-
483
- const x2 = x + (endX - x) * t2;
484
- const y2 = y + (endY - y) * t2;
485
- const z2 = 0.07 + endZ * t2;
486
-
487
- const opacity = 0.25 * (1 - t1) * (1 - t1);
488
-
489
- const rayMat = new THREE.LineBasicMaterial({
490
- color: 0xffffee,
491
- transparent: true,
492
- opacity: opacity
493
- });
494
-
495
- const points = [
496
- new THREE.Vector3(x1, y1, z1),
497
- new THREE.Vector3(x2, y2, z2)
498
- ];
499
- const rayGeo = new THREE.BufferGeometry().setFromPoints(points);
500
- const ray = new THREE.Line(rayGeo, rayMat);
501
- rayGroup.add(ray);
502
- }
503
- });
504
-
505
- // Add subtle cone for overall light volume hint
506
- const coneGeo = new THREE.ConeGeometry(0.6, 2.0, 4, 1, true);
507
- const coneMat = new THREE.MeshBasicMaterial({
508
- color: 0xffffee,
509
  transparent: true,
510
- opacity: 0.03,
511
- side: THREE.DoubleSide
 
 
512
  });
513
- const cone = new THREE.Mesh(coneGeo, coneMat);
514
- cone.rotation.x = -Math.PI / 2;
515
- cone.position.z = 1.0;
516
- rayGroup.add(cone);
517
 
518
- lightGroup.add(rayGroup);
 
 
 
 
519
 
520
  // Actual Light Source
521
- const spotLight = new THREE.SpotLight(0xffffff, 10, 10, Math.PI / 3, 1, 1);
522
- spotLight.position.set(0, 0, -0.05);
523
  spotLight.castShadow = true;
524
  spotLight.shadow.mapSize.width = 1024;
525
  spotLight.shadow.mapSize.height = 1024;
@@ -535,7 +451,9 @@ class LightingControl3D(gr.HTML):
535
 
536
  scene.add(lightGroup);
537
 
538
- // Azimuth ring (Yellow)
 
 
539
  const azimuthRing = new THREE.Mesh(
540
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
541
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
@@ -544,7 +462,7 @@ class LightingControl3D(gr.HTML):
544
  azimuthRing.position.y = 0.05;
545
  scene.add(azimuthRing);
546
 
547
- // Azimuth Handle (Yellow)
548
  const azimuthHandle = new THREE.Mesh(
549
  new THREE.SphereGeometry(0.18, 16, 16),
550
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
@@ -552,7 +470,7 @@ class LightingControl3D(gr.HTML):
552
  azimuthHandle.userData.type = 'azimuth';
553
  scene.add(azimuthHandle);
554
 
555
- // Elevation arc (Blue)
556
  const arcPoints = [];
557
  for (let i = 0; i <= 32; i++) {
558
  const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
@@ -565,7 +483,7 @@ class LightingControl3D(gr.HTML):
565
  );
566
  scene.add(elevationArc);
567
 
568
- // Elevation Handle (Blue)
569
  const elevationHandle = new THREE.Mesh(
570
  new THREE.SphereGeometry(0.18, 16, 16),
571
  new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
@@ -573,13 +491,13 @@ class LightingControl3D(gr.HTML):
573
  elevationHandle.userData.type = 'elevation';
574
  scene.add(elevationHandle);
575
 
576
- // Reset View Button
577
  const refreshBtn = document.createElement('button');
578
  refreshBtn.innerHTML = 'Reset View';
579
  refreshBtn.style.position = 'absolute';
580
  refreshBtn.style.top = '15px';
581
  refreshBtn.style.right = '15px';
582
- refreshBtn.style.background = '#e63e00';
583
  refreshBtn.style.color = '#fff';
584
  refreshBtn.style.border = 'none';
585
  refreshBtn.style.padding = '8px 16px';
@@ -619,6 +537,7 @@ class LightingControl3D(gr.HTML):
619
  azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
620
  elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
621
 
 
622
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
623
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
624
  let prompt = 'Light source from';
@@ -638,6 +557,7 @@ class LightingControl3D(gr.HTML):
638
  trigger('change', props.value);
639
  }
640
 
 
641
  const raycaster = new THREE.Raycaster();
642
  const mouse = new THREE.Vector2();
643
  let isDragging = false;
@@ -710,6 +630,7 @@ class LightingControl3D(gr.HTML):
710
  dragTarget.material.emissiveIntensity = 0.5;
711
  dragTarget.scale.setScalar(1);
712
 
 
713
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
714
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
715
 
@@ -742,7 +663,7 @@ class LightingControl3D(gr.HTML):
742
 
743
  canvas.addEventListener('mouseup', onMouseUp);
744
  canvas.addEventListener('mouseleave', onMouseUp);
745
-
746
  canvas.addEventListener('touchstart', (e) => {
747
  e.preventDefault();
748
  const touch = e.touches[0];
@@ -800,20 +721,24 @@ class LightingControl3D(gr.HTML):
800
  onMouseUp();
801
  }, { passive: false });
802
 
 
803
  updatePositions();
804
 
 
805
  function render() {
806
  requestAnimationFrame(render);
807
  renderer.render(scene, camera);
808
  }
809
  render();
810
 
 
811
  new ResizeObserver(() => {
812
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
813
  camera.updateProjectionMatrix();
814
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
815
  }).observe(wrapper);
816
 
 
817
  wrapper._updateFromProps = (newVal) => {
818
  if (newVal && typeof newVal === 'object') {
819
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
@@ -824,13 +749,16 @@ class LightingControl3D(gr.HTML):
824
 
825
  wrapper._updateTexture = updateTextureFromUrl;
826
 
 
827
  let lastImageUrl = props.imageUrl;
828
  let lastValue = JSON.stringify(props.value);
829
  setInterval(() => {
 
830
  if (props.imageUrl !== lastImageUrl) {
831
  lastImageUrl = props.imageUrl;
832
  updateTextureFromUrl(props.imageUrl);
833
  }
 
834
  const currentValue = JSON.stringify(props.value);
835
  if (currentValue !== lastValue) {
836
  lastValue = currentValue;
@@ -856,11 +784,14 @@ class LightingControl3D(gr.HTML):
856
  )
857
 
858
  css = '''
859
- .progress-text { color: white !important; }
 
 
860
  .slider-row { display: flex; gap: 10px; align-items: center; }
 
861
  '''
862
  with gr.Blocks(css=css) as demo:
863
- gr.Markdown("**Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
864
  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).")
865
 
866
  with gr.Row():
@@ -868,7 +799,7 @@ with gr.Blocks(css=css) as demo:
868
  image = gr.Image(label="Input Image", type="pil", height=300)
869
 
870
  gr.Markdown("### 3D Lighting Control")
871
- gr.Markdown("*Drag the colored handles: Yellow = Azimuth (Direction), Blue = Elevation (Height)*")
872
 
873
  lighting_3d = LightingControl3D(
874
  value={"azimuth": 0, "elevation": 0},
@@ -884,7 +815,7 @@ with gr.Blocks(css=css) as demo:
884
  maximum=315,
885
  step=45,
886
  value=0,
887
- info="0 degrees = front, 90 degrees = right, 180 degrees = rear, 270 degrees = left"
888
  )
889
 
890
  elevation_slider = gr.Slider(
@@ -893,7 +824,7 @@ with gr.Blocks(css=css) as demo:
893
  maximum=90,
894
  step=90,
895
  value=0,
896
- info="-90 degrees = from below, 0 degrees = horizontal, 90 degrees = from above"
897
  )
898
 
899
  with gr.Row():
@@ -907,7 +838,7 @@ with gr.Blocks(css=css) as demo:
907
  with gr.Column(scale=1):
908
  result = gr.Image(label="Output Image", height=500)
909
 
910
- with gr.Accordion("Advanced Settings", open=False):
911
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
912
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
913
  guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
 
234
  const wrapper = element.querySelector('#lighting-control-wrapper');
235
  const promptOverlay = element.querySelector('#prompt-overlay');
236
 
237
+ // Wait for THREE to load
238
  const initScene = () => {
239
  if (typeof THREE === 'undefined') {
240
  setTimeout(initScene, 100);
241
  return;
242
  }
243
 
244
+ // Scene setup
245
  const scene = new THREE.Scene();
246
  scene.background = new THREE.Color(0x1a1a1a);
247
 
 
256
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
257
  wrapper.insertBefore(renderer.domElement, promptOverlay);
258
 
259
+ // Lighting for the scene
260
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
261
 
262
+ // Ground plane for shadows
263
  const ground = new THREE.Mesh(
264
  new THREE.PlaneGeometry(10, 10),
265
  new THREE.ShadowMaterial({ opacity: 0.3 })
 
269
  ground.receiveShadow = true;
270
  scene.add(ground);
271
 
272
+ // Grid
273
  scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
274
 
275
+ // Constants
276
  const CENTER = new THREE.Vector3(0, 0.75, 0);
277
  const BASE_DISTANCE = 2.5;
278
  const AZIMUTH_RADIUS = 2.4;
279
  const ELEVATION_RADIUS = 1.8;
280
 
281
+ // State
282
  let azimuthAngle = props.value?.azimuth || 0;
283
  let elevationAngle = props.value?.elevation || 0;
284
 
285
+ // Mappings
286
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
287
  const elevationSteps = [-90, 0, 90];
288
  const azimuthNames = {
 
296
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
297
  }
298
 
299
+ // Create placeholder texture (smiley face)
300
  function createPlaceholderTexture() {
301
  const canvas = document.createElement('canvas');
302
  canvas.width = 256;
 
321
  return new THREE.CanvasTexture(canvas);
322
  }
323
 
324
+ // Target image plane
325
  let currentTexture = createPlaceholderTexture();
326
  const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
327
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
 
329
  targetPlane.receiveShadow = true;
330
  scene.add(targetPlane);
331
 
332
+ // Function to update texture from image URL
333
  function updateTextureFromUrl(url) {
334
  if (!url) {
335
+ // Reset to placeholder
336
  planeMaterial.map = createPlaceholderTexture();
337
  planeMaterial.needsUpdate = true;
338
+ // Reset plane to square
339
  scene.remove(targetPlane);
340
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
341
  targetPlane.position.copy(CENTER);
 
352
  planeMaterial.map = texture;
353
  planeMaterial.needsUpdate = true;
354
 
355
+ // Adjust plane aspect ratio to match image
356
  const img = texture.image;
357
  if (img && img.width && img.height) {
358
  const aspect = img.width / img.height;
 
379
  });
380
  }
381
 
382
+ // Check for initial imageUrl
383
  if (props.imageUrl) {
384
  updateTextureFromUrl(props.imageUrl);
385
  }
386
 
387
+ // --- NEW LIGHT MODEL: STUDIO SQUARE WITH RAYS ---
388
  const lightGroup = new THREE.Group();
389
+
390
+ // 1. Studio Housing (Black/Dark Grey)
391
+ const housingGeo = new THREE.BoxGeometry(0.7, 0.7, 0.2);
392
+ const housingMat = new THREE.MeshStandardMaterial({
393
+ color: 0x222222,
 
 
 
 
 
 
 
 
 
 
394
  roughness: 0.3,
395
+ metalness: 0.7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  });
397
+ const housing = new THREE.Mesh(housingGeo, housingMat);
398
+
399
+ // 2. Light Surface (Bright White Square)
400
+ const faceGeo = new THREE.PlaneGeometry(0.65, 0.65);
401
+ const faceMat = new THREE.MeshStandardMaterial({
402
+ color: 0xffffff,
403
+ emissive: 0xffffff,
404
+ emissiveIntensity: 3.0,
405
+ roughness: 0.0
 
 
 
 
 
 
 
 
 
406
  });
407
+ const face = new THREE.Mesh(faceGeo, faceMat);
408
+ face.position.z = 0.105; // Slightly in front of housing
409
+ housing.add(face);
410
 
411
+ // 3. Light Rays (Volumetric Beam Effect)
412
+ // Using a transparent cylinder to simulate a beam
413
+ const beamHeight = 4.0;
414
+ // Cylinder top (0.5) is light size, bottom (1.2) is spread
415
+ const beamGeo = new THREE.CylinderGeometry(0.5, 1.2, beamHeight, 32, 1, true);
416
 
417
+ // Shift geometry so narrow end starts at 0 and extends positive
418
+ beamGeo.translate(0, -beamHeight / 2, 0);
419
+ beamGeo.rotateX(-Math.PI / 2); // Rotate to point along +Z axis
 
 
 
 
 
420
 
421
+ const beamMat = new THREE.MeshBasicMaterial({
422
+ color: 0xffffff,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  transparent: true,
424
+ opacity: 0.06,
425
+ side: THREE.DoubleSide,
426
+ depthWrite: false,
427
+ blending: THREE.AdditiveBlending
428
  });
 
 
 
 
429
 
430
+ const beam = new THREE.Mesh(beamGeo, beamMat);
431
+ beam.position.z = 0.12; // Start just in front of the face
432
+ housing.add(beam);
433
+
434
+ lightGroup.add(housing);
435
 
436
  // Actual Light Source
437
+ const spotLight = new THREE.SpotLight(0xffffff, 15, 20, Math.PI / 3, 0.5, 1);
438
+ spotLight.position.set(0, 0, 0.1);
439
  spotLight.castShadow = true;
440
  spotLight.shadow.mapSize.width = 1024;
441
  spotLight.shadow.mapSize.height = 1024;
 
451
 
452
  scene.add(lightGroup);
453
 
454
+ // --- CONTROLS: COLORS UPDATED ---
455
+
456
+ // Azimuth ring (YELLOW)
457
  const azimuthRing = new THREE.Mesh(
458
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
459
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
 
462
  azimuthRing.position.y = 0.05;
463
  scene.add(azimuthRing);
464
 
465
+ // Azimuth Handle (YELLOW)
466
  const azimuthHandle = new THREE.Mesh(
467
  new THREE.SphereGeometry(0.18, 16, 16),
468
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
 
470
  azimuthHandle.userData.type = 'azimuth';
471
  scene.add(azimuthHandle);
472
 
473
+ // Elevation arc (BLUE)
474
  const arcPoints = [];
475
  for (let i = 0; i <= 32; i++) {
476
  const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
 
483
  );
484
  scene.add(elevationArc);
485
 
486
+ // Elevation Handle (BLUE)
487
  const elevationHandle = new THREE.Mesh(
488
  new THREE.SphereGeometry(0.18, 16, 16),
489
  new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
 
491
  elevationHandle.userData.type = 'elevation';
492
  scene.add(elevationHandle);
493
 
494
+ // --- REFRESH BUTTON ---
495
  const refreshBtn = document.createElement('button');
496
  refreshBtn.innerHTML = 'Reset View';
497
  refreshBtn.style.position = 'absolute';
498
  refreshBtn.style.top = '15px';
499
  refreshBtn.style.right = '15px';
500
+ refreshBtn.style.background = '#e63e00'; // Theme primary color
501
  refreshBtn.style.color = '#fff';
502
  refreshBtn.style.border = 'none';
503
  refreshBtn.style.padding = '8px 16px';
 
537
  azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
538
  elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
539
 
540
+ // Update prompt
541
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
542
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
543
  let prompt = 'Light source from';
 
557
  trigger('change', props.value);
558
  }
559
 
560
+ // Raycasting
561
  const raycaster = new THREE.Raycaster();
562
  const mouse = new THREE.Vector2();
563
  let isDragging = false;
 
630
  dragTarget.material.emissiveIntensity = 0.5;
631
  dragTarget.scale.setScalar(1);
632
 
633
+ // Snap and animate
634
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
635
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
636
 
 
663
 
664
  canvas.addEventListener('mouseup', onMouseUp);
665
  canvas.addEventListener('mouseleave', onMouseUp);
666
+ // Touch support for mobile
667
  canvas.addEventListener('touchstart', (e) => {
668
  e.preventDefault();
669
  const touch = e.touches[0];
 
721
  onMouseUp();
722
  }, { passive: false });
723
 
724
+ // Initial update
725
  updatePositions();
726
 
727
+ // Render loop
728
  function render() {
729
  requestAnimationFrame(render);
730
  renderer.render(scene, camera);
731
  }
732
  render();
733
 
734
+ // Handle resize
735
  new ResizeObserver(() => {
736
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
737
  camera.updateProjectionMatrix();
738
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
739
  }).observe(wrapper);
740
 
741
+ // Store update functions for external calls
742
  wrapper._updateFromProps = (newVal) => {
743
  if (newVal && typeof newVal === 'object') {
744
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
 
749
 
750
  wrapper._updateTexture = updateTextureFromUrl;
751
 
752
+ // Watch for prop changes (imageUrl and value)
753
  let lastImageUrl = props.imageUrl;
754
  let lastValue = JSON.stringify(props.value);
755
  setInterval(() => {
756
+ // Check imageUrl changes
757
  if (props.imageUrl !== lastImageUrl) {
758
  lastImageUrl = props.imageUrl;
759
  updateTextureFromUrl(props.imageUrl);
760
  }
761
+ // Check value changes (from sliders)
762
  const currentValue = JSON.stringify(props.value);
763
  if (currentValue !== lastValue) {
764
  lastValue = currentValue;
 
784
  )
785
 
786
  css = '''
787
+ #col-container { max-width: 1200px; margin: 0 auto; }
788
+ .dark .progress-text { color: white !important; }
789
+ #lighting-3d-control { min-height: 450px; }
790
  .slider-row { display: flex; gap: 10px; align-items: center; }
791
+ #main-title h1 {font-size: 2.4em !important;}
792
  '''
793
  with gr.Blocks(css=css) as demo:
794
+ gr.Markdown("# **Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_id="main-title")
795
  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).")
796
 
797
  with gr.Row():
 
799
  image = gr.Image(label="Input Image", type="pil", height=300)
800
 
801
  gr.Markdown("### 3D Lighting Control")
802
+ gr.Markdown("*Drag the colored handles: 🟡 Azimuth (Direction), 🔵 Elevation (Height)*")
803
 
804
  lighting_3d = LightingControl3D(
805
  value={"azimuth": 0, "elevation": 0},
 
815
  maximum=315,
816
  step=45,
817
  value=0,
818
+ info="0°=front, 90°=right, 180°=rear, 270°=left"
819
  )
820
 
821
  elevation_slider = gr.Slider(
 
824
  maximum=90,
825
  step=90,
826
  value=0,
827
+ info="-90°=from below, 0°=horizontal, 90°=from above"
828
  )
829
 
830
  with gr.Row():
 
838
  with gr.Column(scale=1):
839
  result = gr.Image(label="Output Image", height=500)
840
 
841
+ with gr.Accordion("Advanced Settings", open=True):
842
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
843
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
844
  guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)