Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions src/common/model/blockers/LineBlocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,21 @@
* A line-segment blocker that absorbs all rays hitting it.
*/

import { BaseElement } from "../optics/BaseElement.js";
import { dot, type Point, point, raySegmentIntersection, segment, segmentNormal } from "../optics/Geometry.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import { type Point } from "../optics/Geometry.js";
import type {
ElementCategory,
IntersectionResult,
RayInteractionResult,
SimulationRay,
} from "../optics/OpticsTypes.js";

export class LineBlocker extends BaseElement {
export class LineBlocker extends BaseSegmentElement {
public readonly type = "Blocker";
public readonly category: ElementCategory = "blocker";

public p1: Point;
public p2: Point;

public constructor(p1: Point, p2: Point) {
super();
this.p1 = p1;
this.p2 = p2;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
super(p1, p2);
}

public override onRayIncident(_ray: SimulationRay, _intersection: IntersectionResult): RayInteractionResult {
Expand Down
12 changes: 5 additions & 7 deletions src/common/model/glass/IdealLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { DEFAULT_FOCAL_LENGTH } from "../../../OpticsLabConstants.js";
import { BaseElement } from "../optics/BaseElement.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import {
normalize,
type Point,
Expand All @@ -25,21 +25,19 @@ import type {
SimulationRay,
} from "../optics/OpticsTypes.js";

export class IdealLens extends BaseElement {
export class IdealLens extends BaseSegmentElement {
public readonly type = "IdealLens";
public readonly category: ElementCategory = "glass";

public p1: Point;
public p2: Point;
public focalLength: number;

public constructor(p1: Point, p2: Point, focalLength = DEFAULT_FOCAL_LENGTH) {
super();
this.p1 = p1;
this.p2 = p2;
super(p1, p2);
this.focalLength = focalLength;
}

// IdealLens uses an unflipped normal so that the lens equation works correctly
// for rays incident from either side.
public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
Expand Down
31 changes: 4 additions & 27 deletions src/common/model/gratings/ReflectionGrating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,8 @@
* - dutyCycle: slit-width to line-spacing ratio (0–1), controls order intensities
*/

import { BaseElement } from "../optics/BaseElement.js";
import {
dot,
normalize,
type Point,
point,
raySegmentIntersection,
segment,
segmentNormal,
} from "../optics/Geometry.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import { dot, normalize, type Point, point } from "../optics/Geometry.js";
import type {
ElementCategory,
IntersectionResult,
Expand All @@ -33,36 +25,21 @@ const DEFAULT_WAVELENGTH_NM = 532;
/** Maximum diffraction order to compute. */
const MAX_ORDER = 10;

export class ReflectionGrating extends BaseElement {
export class ReflectionGrating extends BaseSegmentElement {
public readonly type = "ReflectionGrating";
public readonly category: ElementCategory = "mirror";

public p1: Point;
public p2: Point;
/** Groove density in lines per mm. */
public linesDensity: number;
/** Slit-width / line-spacing ratio (0–1). */
public dutyCycle: number;

public constructor(p1: Point, p2: Point, linesDensity = 600, dutyCycle = 0.5) {
super();
this.p1 = p1;
this.p2 = p2;
super(p1, p2);
this.linesDensity = linesDensity;
this.dutyCycle = dutyCycle;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
// Orient normal to face incoming ray
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
}

public override onRayIncident(ray: SimulationRay, intersection: IntersectionResult): RayInteractionResult {
const n = intersection.normal;
const wavelengthNm = ray.wavelength ?? DEFAULT_WAVELENGTH_NM;
Expand Down
31 changes: 4 additions & 27 deletions src/common/model/gratings/TransmissionGrating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,8 @@
* - dutyCycle: slit-width to line-spacing ratio (0–1), controls order intensities
*/

import { BaseElement } from "../optics/BaseElement.js";
import {
dot,
normalize,
type Point,
point,
raySegmentIntersection,
segment,
segmentNormal,
} from "../optics/Geometry.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import { dot, normalize, type Point, point } from "../optics/Geometry.js";
import type {
ElementCategory,
IntersectionResult,
Expand All @@ -33,36 +25,21 @@ const DEFAULT_WAVELENGTH_NM = 532;
/** Maximum diffraction order to compute. */
const MAX_ORDER = 10;

export class TransmissionGrating extends BaseElement {
export class TransmissionGrating extends BaseSegmentElement {
public readonly type = "TransmissionGrating";
public readonly category: ElementCategory = "glass";

public p1: Point;
public p2: Point;
/** Groove density in lines per mm. */
public linesDensity: number;
/** Slit-width / line-spacing ratio (0–1). */
public dutyCycle: number;

public constructor(p1: Point, p2: Point, linesDensity = 600, dutyCycle = 0.5) {
super();
this.p1 = p1;
this.p2 = p2;
super(p1, p2);
this.linesDensity = linesDensity;
this.dutyCycle = dutyCycle;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
// Orient normal to face incoming ray
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
}

public override onRayIncident(ray: SimulationRay, intersection: IntersectionResult): RayInteractionResult {
const n = intersection.normal;
const wavelengthNm = ray.wavelength ?? DEFAULT_WAVELENGTH_NM;
Expand Down
1 change: 1 addition & 0 deletions src/common/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export { ParabolicMirror } from "./mirrors/ParabolicMirror.js";
// ── Mirrors ──────────────────────────────────────────────────────────────────
export { SegmentMirror } from "./mirrors/SegmentMirror.js";
export { BaseElement } from "./optics/BaseElement.js";
export { BaseSegmentElement } from "./optics/BaseSegmentElement.js";
// ── Core types & geometry ────────────────────────────────────────────────────
export type { Circle, Line, Point, Segment } from "./optics/Geometry.js";
export {
Expand Down
30 changes: 4 additions & 26 deletions src/common/model/mirrors/BeamSplitterElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,27 @@
* in the original direction.
*/

import { BaseElement } from "../optics/BaseElement.js";
import {
dot,
normalize,
type Point,
point,
raySegmentIntersection,
segment,
segmentNormal,
} from "../optics/Geometry.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import { dot, normalize, type Point, point } from "../optics/Geometry.js";
import type {
ElementCategory,
IntersectionResult,
RayInteractionResult,
SimulationRay,
} from "../optics/OpticsTypes.js";

export class BeamSplitterElement extends BaseElement {
export class BeamSplitterElement extends BaseSegmentElement {
public readonly type = "BeamSplitter";
public readonly category: ElementCategory = "mirror";

public p1: Point;
public p2: Point;
/** Fraction of brightness transmitted (0..1). The rest is reflected. */
public transRatio: number;

public constructor(p1: Point, p2: Point, transRatio = 0.5) {
super();
this.p1 = p1;
this.p2 = p2;
super(p1, p2);
this.transRatio = transRatio;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
}

public override onRayIncident(ray: SimulationRay, intersection: IntersectionResult): RayInteractionResult {
const n = intersection.normal;
const d = ray.direction;
Expand Down
23 changes: 3 additions & 20 deletions src/common/model/mirrors/IdealCurvedMirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@
*/

import { DEFAULT_FOCAL_LENGTH } from "../../../OpticsLabConstants.js";
import { BaseElement } from "../optics/BaseElement.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import {
distanceSquared,
dot,
line,
linesIntersection,
normalize,
type Point,
point,
raySegmentIntersection,
segment,
segmentMidpoint,
segmentNormal,
} from "../optics/Geometry.js";
import type {
ElementCategory,
Expand All @@ -28,31 +25,17 @@ import type {
SimulationRay,
} from "../optics/OpticsTypes.js";

export class IdealCurvedMirror extends BaseElement {
export class IdealCurvedMirror extends BaseSegmentElement {
public readonly type = "IdealMirror";
public readonly category: ElementCategory = "mirror";

public p1: Point;
public p2: Point;
public focalLength: number;

public constructor(p1: Point, p2: Point, focalLength = DEFAULT_FOCAL_LENGTH) {
super();
this.p1 = p1;
this.p2 = p2;
super(p1, p2);
this.focalLength = focalLength;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
}

public override onRayIncident(ray: SimulationRay, intersection: IntersectionResult): RayInteractionResult {
const ip = intersection.point;
const center = segmentMidpoint(segment(this.p1, this.p2));
Expand Down
32 changes: 4 additions & 28 deletions src/common/model/mirrors/SegmentMirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,21 @@
* law of reflection: angle of incidence = angle of reflection.
*/

import { BaseElement } from "../optics/BaseElement.js";
import {
dot,
normalize,
type Point,
point,
raySegmentIntersection,
segment,
segmentNormal,
subtract,
} from "../optics/Geometry.js";
import { BaseSegmentElement } from "../optics/BaseSegmentElement.js";
import { normalize, type Point, point, subtract } from "../optics/Geometry.js";
import type {
ElementCategory,
IntersectionResult,
RayInteractionResult,
SimulationRay,
} from "../optics/OpticsTypes.js";

export class SegmentMirror extends BaseElement {
export class SegmentMirror extends BaseSegmentElement {
public readonly type = "Mirror";
public readonly category: ElementCategory = "mirror";

public p1: Point;
public p2: Point;

public constructor(p1: Point, p2: Point) {
super();
this.p1 = p1;
this.p2 = p2;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
super(p1, p2);
}

public override onRayIncident(ray: SimulationRay, intersection: IntersectionResult): RayInteractionResult {
Expand Down
32 changes: 32 additions & 0 deletions src/common/model/optics/BaseSegmentElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* BaseSegmentElement.ts
*
* Abstract base class for optical elements defined by a line segment (p1, p2).
* Provides shared p1/p2 fields and a default checkRayIntersection that tests
* for a segment hit and returns a normal oriented toward the incoming ray.
*/

import { BaseElement } from "./BaseElement.js";
import { dot, type Point, point, raySegmentIntersection, segment, segmentNormal } from "./Geometry.js";
import type { IntersectionResult, SimulationRay } from "./OpticsTypes.js";

export abstract class BaseSegmentElement extends BaseElement {
public p1: Point;
public p2: Point;

public constructor(p1: Point, p2: Point) {
super();
this.p1 = p1;
this.p2 = p2;
}

public override checkRayIntersection(ray: SimulationRay): IntersectionResult | null {
const hit = raySegmentIntersection(ray.origin, ray.direction, segment(this.p1, this.p2));
if (!hit) {
return null;
}
const normal = segmentNormal(segment(this.p1, this.p2));
const facingRay = dot(normal, ray.direction) < 0 ? normal : point(-normal.x, -normal.y);
return { point: hit.point, t: hit.t, element: this, normal: facingRay };
}
}
Loading