diff --git a/src/common/model/blockers/LineBlocker.ts b/src/common/model/blockers/LineBlocker.ts index f030a8e..bd7d451 100644 --- a/src/common/model/blockers/LineBlocker.ts +++ b/src/common/model/blockers/LineBlocker.ts @@ -4,8 +4,8 @@ * 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, @@ -13,27 +13,12 @@ import type { 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 { diff --git a/src/common/model/glass/IdealLens.ts b/src/common/model/glass/IdealLens.ts index 3d9a3b4..b9cd74e 100644 --- a/src/common/model/glass/IdealLens.ts +++ b/src/common/model/glass/IdealLens.ts @@ -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, @@ -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) { diff --git a/src/common/model/gratings/ReflectionGrating.ts b/src/common/model/gratings/ReflectionGrating.ts index 2cf60c9..528f84d 100644 --- a/src/common/model/gratings/ReflectionGrating.ts +++ b/src/common/model/gratings/ReflectionGrating.ts @@ -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, @@ -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; diff --git a/src/common/model/gratings/TransmissionGrating.ts b/src/common/model/gratings/TransmissionGrating.ts index 882de8a..db87cc7 100644 --- a/src/common/model/gratings/TransmissionGrating.ts +++ b/src/common/model/gratings/TransmissionGrating.ts @@ -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, @@ -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; diff --git a/src/common/model/index.ts b/src/common/model/index.ts index 27e0698..4744417 100644 --- a/src/common/model/index.ts +++ b/src/common/model/index.ts @@ -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 { diff --git a/src/common/model/mirrors/BeamSplitterElement.ts b/src/common/model/mirrors/BeamSplitterElement.ts index ca954a2..256859c 100644 --- a/src/common/model/mirrors/BeamSplitterElement.ts +++ b/src/common/model/mirrors/BeamSplitterElement.ts @@ -7,16 +7,8 @@ * 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, @@ -24,32 +16,18 @@ import type { 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; diff --git a/src/common/model/mirrors/IdealCurvedMirror.ts b/src/common/model/mirrors/IdealCurvedMirror.ts index 075c8ba..4884c36 100644 --- a/src/common/model/mirrors/IdealCurvedMirror.ts +++ b/src/common/model/mirrors/IdealCurvedMirror.ts @@ -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, @@ -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)); diff --git a/src/common/model/mirrors/SegmentMirror.ts b/src/common/model/mirrors/SegmentMirror.ts index 95c6a2a..96d01ba 100644 --- a/src/common/model/mirrors/SegmentMirror.ts +++ b/src/common/model/mirrors/SegmentMirror.ts @@ -5,17 +5,8 @@ * 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, @@ -23,27 +14,12 @@ import type { 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 { diff --git a/src/common/model/optics/BaseSegmentElement.ts b/src/common/model/optics/BaseSegmentElement.ts new file mode 100644 index 0000000..751f963 --- /dev/null +++ b/src/common/model/optics/BaseSegmentElement.ts @@ -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 }; + } +}