prithivMLmods commited on
Commit
dd04450
·
verified ·
1 Parent(s): b56a55c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -111
app.py CHANGED
@@ -15,17 +15,17 @@ from gradio.themes.utils import colors, fonts, sizes
15
 
16
  colors.orange_red = colors.Color(
17
  name="orange_red",
18
- c50="#FFF0E5",
19
- c100="#FFE0CC",
20
- c200="#FFC299",
21
- c300="#FFA366",
22
- c400="#FF8533",
23
- c500="#FF4500",
24
- c600="#E63E00",
25
- c700="#CC3700",
26
- c800="#B33000",
27
- c900="#992900",
28
- c950="#802200",
29
  )
30
 
31
  class OrangeRedTheme(Soft):
@@ -127,21 +127,9 @@ ELEVATION_MAP = {
127
  }
128
 
129
  def snap_to_nearest(value, options):
130
- """Snap a value to the nearest option in a list."""
131
  return min(options, key=lambda x: abs(x - value))
132
 
133
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
134
- """
135
- Build a lighting prompt from azimuth and elevation values.
136
-
137
- Args:
138
- azimuth: Horizontal rotation in degrees (0-360)
139
- elevation: Vertical angle in degrees (-90 to 90)
140
-
141
- Returns:
142
- Formatted prompt string for the LoRA
143
- """
144
- # 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
 
@@ -162,9 +150,6 @@ def infer_lighting_edit(
162
  height: int = 1024,
163
  width: int = 1024,
164
  ):
165
- """
166
- Edit the lighting of an image using Qwen Image Edit 2511 with multi-angle lighting LoRA.
167
- """
168
  global loaded
169
  progress = gr.Progress(track_tqdm=True)
170
 
@@ -198,7 +183,6 @@ def infer_lighting_edit(
198
  return result, seed, prompt
199
 
200
  def update_dimensions_on_upload(image):
201
- """Compute recommended dimensions preserving aspect ratio."""
202
  if image is None:
203
  return 1024, 1024
204
  original_width, original_height = image.size
@@ -215,34 +199,27 @@ def update_dimensions_on_upload(image):
215
  return new_width, new_height
216
 
217
  class LightingControl3D(gr.HTML):
218
- """
219
- A 3D lighting control component using Three.js.
220
- Outputs: { azimuth: number, elevation: number }
221
- Accepts imageUrl prop to display user's uploaded image on the plane.
222
- """
223
  def __init__(self, value=None, imageUrl=None, **kwargs):
224
  if value is None:
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
 
233
  js_on_load = """
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 = () => {
240
  if (typeof THREE === 'undefined') {
241
  setTimeout(initScene, 100);
242
  return;
243
  }
244
 
245
- // Scene setup
246
  const scene = new THREE.Scene();
247
  scene.background = new THREE.Color(0x1a1a1a);
248
 
@@ -257,10 +234,8 @@ class LightingControl3D(gr.HTML):
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 })
@@ -270,20 +245,16 @@ class LightingControl3D(gr.HTML):
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;
284
  let elevationAngle = props.value?.elevation || 0;
285
 
286
- // Mappings
287
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
288
  const elevationSteps = [-90, 0, 90];
289
  const azimuthNames = {
@@ -297,24 +268,23 @@ class LightingControl3D(gr.HTML):
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);
@@ -322,7 +292,6 @@ class LightingControl3D(gr.HTML):
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);
@@ -330,13 +299,10 @@ class LightingControl3D(gr.HTML):
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);
@@ -353,7 +319,6 @@ class LightingControl3D(gr.HTML):
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;
@@ -380,41 +345,189 @@ class LightingControl3D(gr.HTML):
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;
@@ -430,9 +543,6 @@ class LightingControl3D(gr.HTML):
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 })
@@ -441,7 +551,6 @@ class LightingControl3D(gr.HTML):
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 })
@@ -449,7 +558,6 @@ class LightingControl3D(gr.HTML):
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));
@@ -462,7 +570,6 @@ class LightingControl3D(gr.HTML):
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 })
@@ -470,14 +577,13 @@ class LightingControl3D(gr.HTML):
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';
@@ -489,8 +595,8 @@ class LightingControl3D(gr.HTML):
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
 
@@ -501,6 +607,8 @@ class LightingControl3D(gr.HTML):
501
  updatePropsAndTrigger();
502
  });
503
 
 
 
504
  function updatePositions() {
505
  const distance = BASE_DISTANCE;
506
  const azRad = THREE.MathUtils.degToRad(azimuthAngle);
@@ -516,7 +624,6 @@ class LightingControl3D(gr.HTML):
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);
521
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
522
  let prompt = 'Light source from';
@@ -536,7 +643,6 @@ class LightingControl3D(gr.HTML):
536
  trigger('change', props.value);
537
  }
538
 
539
- // Raycasting
540
  const raycaster = new THREE.Raycaster();
541
  const mouse = new THREE.Vector2();
542
  let isDragging = false;
@@ -609,7 +715,6 @@ class LightingControl3D(gr.HTML):
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,7 +747,7 @@ class LightingControl3D(gr.HTML):
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];
@@ -700,24 +805,43 @@ class LightingControl3D(gr.HTML):
700
  onMouseUp();
701
  }, { passive: false });
702
 
703
- // Initial update
704
  updatePositions();
705
 
706
- // Render loop
707
  function render() {
708
  requestAnimationFrame(render);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  renderer.render(scene, camera);
710
  }
711
  render();
712
 
713
- // Handle resize
714
  new ResizeObserver(() => {
715
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
716
  camera.updateProjectionMatrix();
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,16 +852,13 @@ class LightingControl3D(gr.HTML):
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;
@@ -763,14 +884,14 @@ class LightingControl3D(gr.HTML):
763
  )
764
 
765
  css = '''
766
- #col-container { max-width: 1200px; margin: 0 auto; }
767
  .dark .progress-text { color: white !important; }
768
- #lighting-3d-control { min-height: 450px; }
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).")
775
 
776
  with gr.Row():
@@ -782,7 +903,7 @@ with gr.Blocks(css=css) as demo:
782
 
783
  lighting_3d = LightingControl3D(
784
  value={"azimuth": 0, "elevation": 0},
785
- elem_id="lighting-3d-control"
786
  )
787
  run_btn = gr.Button("Generate Image", variant="primary", size="lg")
788
 
@@ -826,12 +947,10 @@ with gr.Blocks(css=css) as demo:
826
  width = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
827
 
828
  def update_prompt_from_sliders(azimuth, elevation):
829
- """Update prompt preview when sliders change."""
830
  prompt = build_lighting_prompt(azimuth, elevation)
831
  return prompt
832
 
833
  def sync_3d_to_sliders(lighting_value):
834
- """Sync 3D control changes to sliders."""
835
  if lighting_value and isinstance(lighting_value, dict):
836
  az = lighting_value.get('azimuth', 0)
837
  el = lighting_value.get('elevation', 0)
@@ -840,11 +959,9 @@ with gr.Blocks(css=css) as demo:
840
  return gr.update(), gr.update(), gr.update()
841
 
842
  def sync_sliders_to_3d(azimuth, elevation):
843
- """Sync slider changes to 3D control."""
844
  return {"azimuth": azimuth, "elevation": elevation}
845
 
846
  def update_3d_image(image):
847
- """Update the 3D component with the uploaded image."""
848
  if image is None:
849
  return gr.update(imageUrl=None)
850
 
 
15
 
16
  colors.orange_red = colors.Color(
17
  name="orange_red",
18
+ c50="rgb(255, 240, 229)",
19
+ c100="rgb(255, 224, 204)",
20
+ c200="rgb(255, 194, 153)",
21
+ c300="rgb(255, 163, 102)",
22
+ c400="rgb(255, 133, 51)",
23
+ c500="rgb(255, 69, 0)",
24
+ c600="rgb(230, 62, 0)",
25
+ c700="rgb(204, 55, 0)",
26
+ c800="rgb(179, 48, 0)",
27
+ c900="rgb(153, 41, 0)",
28
+ c950="rgb(128, 34, 0)",
29
  )
30
 
31
  class OrangeRedTheme(Soft):
 
127
  }
128
 
129
  def snap_to_nearest(value, options):
 
130
  return min(options, key=lambda x: abs(x - value))
131
 
132
  def build_lighting_prompt(azimuth: float, elevation: float) -> str:
 
 
 
 
 
 
 
 
 
 
 
133
  azimuth_snapped = snap_to_nearest(azimuth, list(AZIMUTH_MAP.keys()))
134
  elevation_snapped = snap_to_nearest(elevation, list(ELEVATION_MAP.keys()))
135
 
 
150
  height: int = 1024,
151
  width: int = 1024,
152
  ):
 
 
 
153
  global loaded
154
  progress = gr.Progress(track_tqdm=True)
155
 
 
183
  return result, seed, prompt
184
 
185
  def update_dimensions_on_upload(image):
 
186
  if image is None:
187
  return 1024, 1024
188
  original_width, original_height = image.size
 
199
  return new_width, new_height
200
 
201
  class LightingControl3D(gr.HTML):
 
 
 
 
 
202
  def __init__(self, value=None, imageUrl=None, **kwargs):
203
  if value is None:
204
  value = {"azimuth": 0, "elevation": 0}
205
 
206
  html_template = """
207
+ <div id="lighting-control-wrapper" style="width: 100%; height: 450px; position: relative; background: rgb(26, 26, 26); border-radius: 12px; overflow: hidden;">
208
+ <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: rgb(0, 255, 136); white-space: nowrap; z-index: 10;"></div>
209
  </div>
210
  """
211
 
212
  js_on_load = """
213
  (() => {
214
+ const wrapper = element.querySelector('[id="lighting-control-wrapper"]');
215
+ const promptOverlay = element.querySelector('[id="prompt-overlay"]');
216
 
 
217
  const initScene = () => {
218
  if (typeof THREE === 'undefined') {
219
  setTimeout(initScene, 100);
220
  return;
221
  }
222
 
 
223
  const scene = new THREE.Scene();
224
  scene.background = new THREE.Color(0x1a1a1a);
225
 
 
234
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
235
  wrapper.insertBefore(renderer.domElement, promptOverlay);
236
 
 
237
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
238
 
 
239
  const ground = new THREE.Mesh(
240
  new THREE.PlaneGeometry(10, 10),
241
  new THREE.ShadowMaterial({ opacity: 0.3 })
 
245
  ground.receiveShadow = true;
246
  scene.add(ground);
247
 
 
248
  scene.add(new THREE.GridHelper(8, 16, 0x333333, 0x222222));
249
 
 
250
  const CENTER = new THREE.Vector3(0, 0.75, 0);
251
  const BASE_DISTANCE = 2.5;
252
  const AZIMUTH_RADIUS = 2.4;
253
  const ELEVATION_RADIUS = 1.8;
254
 
 
255
  let azimuthAngle = props.value?.azimuth || 0;
256
  let elevationAngle = props.value?.elevation || 0;
257
 
 
258
  const azimuthSteps = [0, 45, 90, 135, 180, 225, 270, 315];
259
  const elevationSteps = [-90, 0, 90];
260
  const azimuthNames = {
 
268
  return steps.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
269
  }
270
 
 
271
  function createPlaceholderTexture() {
272
  const canvas = document.createElement('canvas');
273
  canvas.width = 256;
274
  canvas.height = 256;
275
  const ctx = canvas.getContext('2d');
276
+ ctx.fillStyle = 'rgb(58, 58, 74)';
277
  ctx.fillRect(0, 0, 256, 256);
278
+ ctx.fillStyle = 'rgb(255, 204, 153)';
279
  ctx.beginPath();
280
  ctx.arc(128, 128, 80, 0, Math.PI * 2);
281
  ctx.fill();
282
+ ctx.fillStyle = 'rgb(51, 51, 51)';
283
  ctx.beginPath();
284
  ctx.arc(100, 110, 10, 0, Math.PI * 2);
285
  ctx.arc(156, 110, 10, 0, Math.PI * 2);
286
  ctx.fill();
287
+ ctx.strokeStyle = 'rgb(51, 51, 51)';
288
  ctx.lineWidth = 3;
289
  ctx.beginPath();
290
  ctx.arc(128, 130, 35, 0.2, Math.PI - 0.2);
 
292
  return new THREE.CanvasTexture(canvas);
293
  }
294
 
 
295
  let currentTexture = createPlaceholderTexture();
296
  const planeMaterial = new THREE.MeshStandardMaterial({ map: currentTexture, side: THREE.DoubleSide, roughness: 0.5, metalness: 0 });
297
  let targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
 
299
  targetPlane.receiveShadow = true;
300
  scene.add(targetPlane);
301
 
 
302
  function updateTextureFromUrl(url) {
303
  if (!url) {
 
304
  planeMaterial.map = createPlaceholderTexture();
305
  planeMaterial.needsUpdate = true;
 
306
  scene.remove(targetPlane);
307
  targetPlane = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), planeMaterial);
308
  targetPlane.position.copy(CENTER);
 
319
  planeMaterial.map = texture;
320
  planeMaterial.needsUpdate = true;
321
 
 
322
  const img = texture.image;
323
  if (img && img.width && img.height) {
324
  const aspect = img.width / img.height;
 
345
  });
346
  }
347
 
 
348
  if (props.imageUrl) {
349
  updateTextureFromUrl(props.imageUrl);
350
  }
351
 
 
352
  const lightGroup = new THREE.Group();
353
+
354
+ const handleGeo = new THREE.CylinderGeometry(0.06, 0.07, 0.45, 16);
355
+ const handleMat = new THREE.MeshStandardMaterial({
356
+ color: 0x2a2a2a,
357
+ roughness: 0.4,
358
+ metalness: 0.7
359
+ });
360
+ const handle = new THREE.Mesh(handleGeo, handleMat);
361
+ handle.rotation.x = Math.PI / 2;
362
+ handle.position.z = -0.35;
363
+ lightGroup.add(handle);
364
+
365
+ const gripGeo = new THREE.CylinderGeometry(0.065, 0.065, 0.3, 16);
366
+ const gripMat = new THREE.MeshStandardMaterial({
367
+ color: 0x1a1a1a,
368
+ roughness: 0.8,
369
+ metalness: 0.2
370
+ });
371
+ const grip = new THREE.Mesh(gripGeo, gripMat);
372
+ grip.rotation.x = Math.PI / 2;
373
+ grip.position.z = -0.35;
374
+ lightGroup.add(grip);
375
+
376
+ for (let i = 0; i < 8; i++) {
377
+ const ringGeo = new THREE.TorusGeometry(0.068, 0.008, 8, 16);
378
+ const ringMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6, metalness: 0.3 });
379
+ const ring = new THREE.Mesh(ringGeo, ringMat);
380
+ ring.position.z = -0.5 + i * 0.035;
381
+ lightGroup.add(ring);
382
+ }
383
+
384
+ const headGeo = new THREE.CylinderGeometry(0.18, 0.08, 0.25, 24);
385
+ const headMat = new THREE.MeshStandardMaterial({
386
+ color: 0x444444,
387
+ roughness: 0.3,
388
+ metalness: 0.8
389
+ });
390
+ const head = new THREE.Mesh(headGeo, headMat);
391
+ head.rotation.x = Math.PI / 2;
392
+ head.position.z = 0;
393
+ lightGroup.add(head);
394
+
395
+ const rimGeo = new THREE.TorusGeometry(0.18, 0.02, 16, 32);
396
+ const rimMat = new THREE.MeshStandardMaterial({
397
+ color: 0x666666,
398
+ roughness: 0.2,
399
+ metalness: 0.9
400
+ });
401
+ const rim = new THREE.Mesh(rimGeo, rimMat);
402
+ rim.position.z = 0.12;
403
+ lightGroup.add(rim);
404
+
405
+ const reflectorGeo = new THREE.ConeGeometry(0.15, 0.18, 24, 1, true);
406
+ const reflectorMat = new THREE.MeshStandardMaterial({
407
+ color: 0xcccccc,
408
+ roughness: 0.1,
409
+ metalness: 1.0,
410
+ side: THREE.BackSide
411
+ });
412
+ const reflector = new THREE.Mesh(reflectorGeo, reflectorMat);
413
+ reflector.rotation.x = -Math.PI / 2;
414
+ reflector.position.z = 0.03;
415
+ lightGroup.add(reflector);
416
+
417
+ const lensGeo = new THREE.CircleGeometry(0.12, 32);
418
+ const lensMat = new THREE.MeshStandardMaterial({
419
+ color: 0xffffee,
420
+ emissive: 0xffffaa,
421
+ emissiveIntensity: 4.0,
422
+ transparent: true,
423
+ opacity: 0.95
424
+ });
425
+ const lens = new THREE.Mesh(lensGeo, lensMat);
426
+ lens.position.z = 0.13;
427
+ lightGroup.add(lens);
428
+
429
+ const glowGeo = new THREE.CircleGeometry(0.18, 32);
430
+ const glowMat = new THREE.MeshBasicMaterial({
431
+ color: 0xffffcc,
432
+ transparent: true,
433
+ opacity: 0.3
434
+ });
435
+ const glow = new THREE.Mesh(glowGeo, glowMat);
436
+ glow.position.z = 0.14;
437
+ lightGroup.add(glow);
438
+
439
+ const rayGroup = new THREE.Group();
440
+
441
+ const coreRayGeo = new THREE.ConeGeometry(0.08, 2.0, 32, 1, true);
442
+ const coreRayMat = new THREE.MeshBasicMaterial({
443
+ color: 0xffffee,
444
+ transparent: true,
445
+ opacity: 0.25,
446
+ side: THREE.DoubleSide
447
+ });
448
+ const coreRay = new THREE.Mesh(coreRayGeo, coreRayMat);
449
+ coreRay.rotation.x = -Math.PI / 2;
450
+ coreRay.position.z = 1.15;
451
+ rayGroup.add(coreRay);
452
+
453
+ const midRayGeo = new THREE.ConeGeometry(0.25, 2.2, 32, 1, true);
454
+ const midRayMat = new THREE.MeshBasicMaterial({
455
+ color: 0xffffaa,
456
+ transparent: true,
457
+ opacity: 0.12,
458
+ side: THREE.DoubleSide
459
+ });
460
+ const midRay = new THREE.Mesh(midRayGeo, midRayMat);
461
+ midRay.rotation.x = -Math.PI / 2;
462
+ midRay.position.z = 1.25;
463
+ rayGroup.add(midRay);
464
+
465
+ const outerRayGeo = new THREE.ConeGeometry(0.5, 2.5, 32, 1, true);
466
+ const outerRayMat = new THREE.MeshBasicMaterial({
467
+ color: 0xffff88,
468
+ transparent: true,
469
+ opacity: 0.06,
470
+ side: THREE.DoubleSide
471
  });
472
+ const outerRay = new THREE.Mesh(outerRayGeo, outerRayMat);
473
+ outerRay.rotation.x = -Math.PI / 2;
474
+ outerRay.position.z = 1.4;
475
+ rayGroup.add(outerRay);
476
+
477
+ const numBeams = 12;
478
+ for (let i = 0; i < numBeams; i++) {
479
+ const angle = (i / numBeams) * Math.PI * 2;
480
+ const beamLength = 1.8 + Math.random() * 0.4;
481
+ const beamGeo = new THREE.CylinderGeometry(0.008, 0.002, beamLength, 8);
482
+ const beamMat = new THREE.MeshBasicMaterial({
483
+ color: 0xffffcc,
484
+ transparent: true,
485
+ opacity: 0.15 + Math.random() * 0.1
486
+ });
487
+ const beam = new THREE.Mesh(beamGeo, beamMat);
488
+ beam.rotation.x = -Math.PI / 2;
489
+ const spread = 0.15;
490
+ beam.position.set(
491
+ Math.cos(angle) * spread,
492
+ Math.sin(angle) * spread,
493
+ 0.15 + beamLength / 2
494
+ );
495
+ beam.rotation.y = Math.cos(angle) * 0.1;
496
+ beam.rotation.x += Math.sin(angle) * 0.1;
497
+ rayGroup.add(beam);
498
+ }
499
+
500
+ const particleCount = 30;
501
+ const particlePositions = [];
502
+ const particleSizes = [];
503
+ for (let i = 0; i < particleCount; i++) {
504
+ const t = Math.random();
505
+ const angle = Math.random() * Math.PI * 2;
506
+ const radius = Math.random() * 0.3 * t;
507
+ particlePositions.push(
508
+ Math.cos(angle) * radius,
509
+ Math.sin(angle) * radius,
510
+ 0.3 + t * 1.8
511
+ );
512
+ particleSizes.push(0.02 + Math.random() * 0.02);
513
+ }
514
+
515
+ const particleGeo = new THREE.BufferGeometry();
516
+ particleGeo.setAttribute('position', new THREE.Float32BufferAttribute(particlePositions, 3));
517
+ const particleMat = new THREE.PointsMaterial({
518
+ color: 0xffffee,
519
+ size: 0.03,
520
+ transparent: true,
521
+ opacity: 0.4,
522
+ sizeAttenuation: true
523
  });
524
+ const particles = new THREE.Points(particleGeo, particleMat);
525
+ rayGroup.add(particles);
526
 
527
+ lightGroup.add(rayGroup);
 
 
528
 
529
+ const spotLight = new THREE.SpotLight(0xffffee, 12, 10, Math.PI / 4, 0.5, 1);
530
+ spotLight.position.set(0, 0, 0.1);
 
531
  spotLight.castShadow = true;
532
  spotLight.shadow.mapSize.width = 1024;
533
  spotLight.shadow.mapSize.height = 1024;
 
543
 
544
  scene.add(lightGroup);
545
 
 
 
 
546
  const azimuthRing = new THREE.Mesh(
547
  new THREE.TorusGeometry(AZIMUTH_RADIUS, 0.04, 16, 64),
548
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.3 })
 
551
  azimuthRing.position.y = 0.05;
552
  scene.add(azimuthRing);
553
 
 
554
  const azimuthHandle = new THREE.Mesh(
555
  new THREE.SphereGeometry(0.18, 16, 16),
556
  new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 })
 
558
  azimuthHandle.userData.type = 'azimuth';
559
  scene.add(azimuthHandle);
560
 
 
561
  const arcPoints = [];
562
  for (let i = 0; i <= 32; i++) {
563
  const angle = THREE.MathUtils.degToRad(-90 + (180 * i / 32));
 
570
  );
571
  scene.add(elevationArc);
572
 
 
573
  const elevationHandle = new THREE.Mesh(
574
  new THREE.SphereGeometry(0.18, 16, 16),
575
  new THREE.MeshStandardMaterial({ color: 0x0000ff, emissive: 0x0000ff, emissiveIntensity: 0.5 })
 
577
  elevationHandle.userData.type = 'elevation';
578
  scene.add(elevationHandle);
579
 
 
580
  const refreshBtn = document.createElement('button');
581
  refreshBtn.innerHTML = 'Reset View';
582
  refreshBtn.style.position = 'absolute';
583
  refreshBtn.style.top = '15px';
584
  refreshBtn.style.right = '15px';
585
+ refreshBtn.style.background = 'rgb(230, 62, 0)';
586
+ refreshBtn.style.color = 'rgb(255, 255, 255)';
587
  refreshBtn.style.border = 'none';
588
  refreshBtn.style.padding = '8px 16px';
589
  refreshBtn.style.borderRadius = '6px';
 
595
  refreshBtn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
596
  refreshBtn.style.transition = 'background 0.2s';
597
 
598
+ refreshBtn.onmouseover = () => refreshBtn.style.background = 'rgb(255, 87, 34)';
599
+ refreshBtn.onmouseout = () => refreshBtn.style.background = 'rgb(230, 62, 0)';
600
 
601
  wrapper.appendChild(refreshBtn);
602
 
 
607
  updatePropsAndTrigger();
608
  });
609
 
610
+ let time = 0;
611
+
612
  function updatePositions() {
613
  const distance = BASE_DISTANCE;
614
  const azRad = THREE.MathUtils.degToRad(azimuthAngle);
 
624
  azimuthHandle.position.set(AZIMUTH_RADIUS * Math.sin(azRad), 0.05, AZIMUTH_RADIUS * Math.cos(azRad));
625
  elevationHandle.position.set(-0.8, ELEVATION_RADIUS * Math.sin(elRad) + CENTER.y, ELEVATION_RADIUS * Math.cos(elRad));
626
 
 
627
  const azSnap = snapToNearest(azimuthAngle, azimuthSteps);
628
  const elSnap = snapToNearest(elevationAngle, elevationSteps);
629
  let prompt = 'Light source from';
 
643
  trigger('change', props.value);
644
  }
645
 
 
646
  const raycaster = new THREE.Raycaster();
647
  const mouse = new THREE.Vector2();
648
  let isDragging = false;
 
715
  dragTarget.material.emissiveIntensity = 0.5;
716
  dragTarget.scale.setScalar(1);
717
 
 
718
  const targetAz = snapToNearest(azimuthAngle, azimuthSteps);
719
  const targetEl = snapToNearest(elevationAngle, elevationSteps);
720
 
 
747
 
748
  canvas.addEventListener('mouseup', onMouseUp);
749
  canvas.addEventListener('mouseleave', onMouseUp);
750
+
751
  canvas.addEventListener('touchstart', (e) => {
752
  e.preventDefault();
753
  const touch = e.touches[0];
 
805
  onMouseUp();
806
  }, { passive: false });
807
 
 
808
  updatePositions();
809
 
 
810
  function render() {
811
  requestAnimationFrame(render);
812
+
813
+ time += 0.016;
814
+
815
+ lensMat.emissiveIntensity = 3.5 + Math.sin(time * 3) * 0.5;
816
+ glowMat.opacity = 0.25 + Math.sin(time * 2) * 0.1;
817
+
818
+ coreRayMat.opacity = 0.22 + Math.sin(time * 4) * 0.03;
819
+ midRayMat.opacity = 0.10 + Math.sin(time * 3 + 1) * 0.02;
820
+ outerRayMat.opacity = 0.05 + Math.sin(time * 2 + 2) * 0.015;
821
+
822
+ const positions = particles.geometry.attributes.position.array;
823
+ for (let i = 0; i < particleCount; i++) {
824
+ positions[i * 3 + 2] += 0.01;
825
+ if (positions[i * 3 + 2] > 2.2) {
826
+ positions[i * 3 + 2] = 0.3;
827
+ const angle = Math.random() * Math.PI * 2;
828
+ const radius = Math.random() * 0.1;
829
+ positions[i * 3] = Math.cos(angle) * radius;
830
+ positions[i * 3 + 1] = Math.sin(angle) * radius;
831
+ }
832
+ }
833
+ particles.geometry.attributes.position.needsUpdate = true;
834
+
835
  renderer.render(scene, camera);
836
  }
837
  render();
838
 
 
839
  new ResizeObserver(() => {
840
  camera.aspect = wrapper.clientWidth / wrapper.clientHeight;
841
  camera.updateProjectionMatrix();
842
  renderer.setSize(wrapper.clientWidth, wrapper.clientHeight);
843
  }).observe(wrapper);
844
 
 
845
  wrapper._updateFromProps = (newVal) => {
846
  if (newVal && typeof newVal === 'object') {
847
  azimuthAngle = newVal.azimuth ?? azimuthAngle;
 
852
 
853
  wrapper._updateTexture = updateTextureFromUrl;
854
 
 
855
  let lastImageUrl = props.imageUrl;
856
  let lastValue = JSON.stringify(props.value);
857
  setInterval(() => {
 
858
  if (props.imageUrl !== lastImageUrl) {
859
  lastImageUrl = props.imageUrl;
860
  updateTextureFromUrl(props.imageUrl);
861
  }
 
862
  const currentValue = JSON.stringify(props.value);
863
  if (currentValue !== lastValue) {
864
  lastValue = currentValue;
 
884
  )
885
 
886
  css = '''
887
+ .col-container { max-width: 1200px; margin: 0 auto; }
888
  .dark .progress-text { color: white !important; }
889
+ .lighting-3d-control { min-height: 450px; }
890
  .slider-row { display: flex; gap: 10px; align-items: center; }
891
+ .main-title h1 {font-size: 2.4em !important;}
892
  '''
893
  with gr.Blocks(css=css) as demo:
894
+ gr.Markdown("**Qwen-Image-Edit-2511-3D-Lighting-Control**", elem_classes=["main-title"])
895
  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).")
896
 
897
  with gr.Row():
 
903
 
904
  lighting_3d = LightingControl3D(
905
  value={"azimuth": 0, "elevation": 0},
906
+ elem_classes=["lighting-3d-control"]
907
  )
908
  run_btn = gr.Button("Generate Image", variant="primary", size="lg")
909
 
 
947
  width = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
948
 
949
  def update_prompt_from_sliders(azimuth, elevation):
 
950
  prompt = build_lighting_prompt(azimuth, elevation)
951
  return prompt
952
 
953
  def sync_3d_to_sliders(lighting_value):
 
954
  if lighting_value and isinstance(lighting_value, dict):
955
  az = lighting_value.get('azimuth', 0)
956
  el = lighting_value.get('elevation', 0)
 
959
  return gr.update(), gr.update(), gr.update()
960
 
961
  def sync_sliders_to_3d(azimuth, elevation):
 
962
  return {"azimuth": azimuth, "elevation": elevation}
963
 
964
  def update_3d_image(image):
 
965
  if image is None:
966
  return gr.update(imageUrl=None)
967