diff --git a/frontend/js/labs/opticsbench.js b/frontend/js/labs/opticsbench.js index 21a0b4e..e072c2b 100644 --- a/frontend/js/labs/opticsbench.js +++ b/frontend/js/labs/opticsbench.js @@ -2545,7 +2545,7 @@ class BenchSim { this._drag = null; this._nextId = 1; // source: object arrow by default. `ang` (deg) aims point/single/laser/parallel. - this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9, ang: 0 }; + this.source = { kind: 'object', xf: 0.07, yf: 0, h: 70, spread: 0.32, rays: 9, ang: 0 }; // elements along the bench, positioned by x-fraction; centred on the axis this.elements = [ this._mk('lens', { xf: 0.40, f: 130, ap: 95 }), @@ -2612,10 +2612,11 @@ class BenchSim { /* ── geometry helpers ── */ _ex(el) { return el.xf * this.W; } _ay() { return this.H / 2; } + _sy() { return this._ay() + (this.source.yf || 0) * (this.H / 2 - 14); } // source y (vertical position) /* Emit the initial rays from the source. */ _emitRays() { - const ay = this._ay(); + const ay = this._sy(); // emission height respects the source vertical position const sx = this.source.xf * this.W; const rays = []; // white light → one sub-ray per spectral sample (they coincide until a prism disperses them) @@ -2795,7 +2796,9 @@ class BenchSim { this._drawScreenHits(ctx, rays); if (typeof _drawOBFXLayer === 'function') { - _drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: ay - (this.source.h || 0) }); + // FX anchored at the actual source point (only the object arrow has a raised tip) + const fxY = this._sy() - (this.source.kind === 'object' ? this.source.h : 0); + _drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: fxY }); } } @@ -2819,7 +2822,8 @@ class BenchSim { ctx.restore(); } - _drawSource(ctx, ay) { + _drawSource(ctx, _ayIgnored) { + const ay = this._sy(); // draw at the source vertical position const sx = this.source.xf * this.W; const aim = (this.source.ang || 0) * Math.PI / 180; ctx.save(); @@ -2938,11 +2942,12 @@ class BenchSim { }; const hit = (mx, my) => { const ay = this._ay(); + // source first (it can sit off-axis), grab around its actual vertical position + const sx = this.source.xf * this.W, sy = this._sy(); + if (Math.abs(mx - sx) < 16 && Math.abs(my - sy) < 70) return { kind: 'src' }; for (const el of this.elements) { if (Math.abs(mx - this._ex(el)) < 14 && Math.abs(my - ay) < 120) return { kind: 'el', id: el.id }; } - const sx = this.source.xf * this.W; - if (Math.abs(mx - sx) < 16 && Math.abs(my - ay) < 120) return { kind: 'src' }; return null; }; on(cv, 'pointerdown', e => { @@ -2957,10 +2962,14 @@ class BenchSim { }); on(cv, 'pointermove', e => { if (!this._drag) { const { mx, my } = pos(e); cv.style.cursor = hit(mx, my) ? 'grab' : 'default'; return; } - const { mx } = pos(e); + const { mx, my } = pos(e); const xf = Math.max(0.02, Math.min(0.98, mx / this.W)); - if (this._drag.kind === 'src') this.source.xf = xf; - else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; } + if (this._drag.kind === 'src') { + this.source.xf = xf; + // vertical drag too → move the source up/down off the axis + const yf = (my - this._ay()) / (this.H / 2 - 14); + this.source.yf = Math.max(-0.95, Math.min(0.95, yf)); + } else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; } this._redraw(); // position drag → redraw canvas, keep inspector intact }); on(cv, 'pointerup', e => { this._drag = null; try { cv.releasePointerCapture(e.pointerId); } catch (_) {} }); @@ -4444,9 +4453,9 @@ function _benchPropsHTML() { let h = '