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
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 97 additions & 1 deletion src/rayTracing/model/SimModel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,104 @@
import type { Tandem } from "scenerystack/tandem";
import opticsLab from "../../OpticsLabNamespace.js";
import { CircleGlass } from "./glass/CircleGlass.js";
import { HalfPlaneGlass } from "./glass/HalfPlaneGlass.js";
import { IdealLens } from "./glass/IdealLens.js";
import { PolygonGlass } from "./glass/PolygonGlass.js";
import { SphericalLens } from "./glass/SphericalLens.js";
import { ArcMirror } from "./mirrors/ArcMirror.js";
import { BeamSplitterElement } from "./mirrors/BeamSplitterElement.js";
import { IdealCurvedMirror } from "./mirrors/IdealCurvedMirror.js";
import { ParabolicMirror } from "./mirrors/ParabolicMirror.js";
import { SegmentMirror } from "./mirrors/SegmentMirror.js";
import { OpticsScene } from "./optics/OpticsScene.js";

// ── Demo scene layout ─────────────────────────────────────────────────────────
// Screen dimensions: 1024 × 618 (typical SceneryStack ScreenView layout bounds).
// Mirrors on the left half, glass / lenses on the right half.

function buildDemoScene(): OpticsScene {
const scene = new OpticsScene();

// ── Mirrors (left column) ─────────────────────────────────────────────────

// 1. Flat segment mirror — horizontal, near top-left
scene.addElement(new SegmentMirror({ x: 80, y: 90 }, { x: 320, y: 90 }));

// 2. Concave arc mirror — circular arc opening upward
scene.addElement(
new ArcMirror(
{ x: 80, y: 210 },
{ x: 320, y: 210 },
{ x: 200, y: 160 }, // control point above midpoint → concave side up
),
);

// 3. Parabolic mirror — parabola opening upward
scene.addElement(
new ParabolicMirror(
{ x: 80, y: 330 },
{ x: 320, y: 330 },
{ x: 200, y: 275 }, // vertex above midpoint → concave side up
),
);

// 4. Ideal curved mirror (obeys mirror equation exactly)
scene.addElement(new IdealCurvedMirror({ x: 80, y: 450 }, { x: 320, y: 450 }, /* focalLength= */ 80));

// 5. Beam splitter — diagonal line in the lower-left
scene.addElement(new BeamSplitterElement({ x: 100, y: 540 }, { x: 300, y: 580 }, /* transRatio= */ 0.5));

// ── Glass / Lenses (right half) ───────────────────────────────────────────

// 6. Circular glass element
scene.addElement(
new CircleGlass(
{ x: 680, y: 120 }, // center
{ x: 745, y: 120 }, // point on boundary (radius ≈ 65 px)
/* refIndex= */ 1.5,
),
);

// 7. Ideal thin lens — vertical segment with converging arrows
scene.addElement(new IdealLens({ x: 860, y: 60 }, { x: 860, y: 200 }, /* focalLength= */ 120));

// 8. Half-plane glass — horizontal boundary with hatching below it
scene.addElement(new HalfPlaneGlass({ x: 560, y: 290 }, { x: 960, y: 290 }, /* refIndex= */ 1.5));

// 9. Spherical biconvex lens — two circular arc surfaces
// axisP1/axisP2 define the optical axis; their distance sets the aperture.
scene.addElement(
new SphericalLens(
{ x: 680, y: 380 }, // axisP1
{ x: 680, y: 520 }, // axisP2 (axis runs vertically; aperture ≈ 140 px)
/* r1= */ 120,
/* r2= */ -120,
/* refIndex= */ 1.5,
),
);

// 10. Triangular prism (polygon glass)
scene.addElement(
new PolygonGlass(
[
{ x: 870, y: 350 },
{ x: 970, y: 530 },
{ x: 770, y: 530 },
],
/* refIndex= */ 1.5,
),
);

return scene;
}

export class SimModel {
public constructor(public readonly tandem: Tandem) {}
/** The central optics scene containing all demo optical elements. */
public readonly scene: OpticsScene;

public constructor(public readonly tandem: Tandem) {
this.scene = buildDemoScene();
}

public reset(): void {
// Called when the user presses the reset-all button
Expand Down
73 changes: 73 additions & 0 deletions src/rayTracing/view/OpticalElementViewFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* OpticalElementViewFactory.ts
*
* Factory that creates the appropriate Scenery Node for any OpticalElement.
* Maps model types to their corresponding view classes.
*/

import type { Node } from "scenerystack/scenery";
import opticsLab from "../../OpticsLabNamespace.js";
import { CircleGlass } from "../model/glass/CircleGlass.js";
import { HalfPlaneGlass } from "../model/glass/HalfPlaneGlass.js";
import { IdealLens } from "../model/glass/IdealLens.js";
import { PolygonGlass } from "../model/glass/PolygonGlass.js";
import { ArcMirror } from "../model/mirrors/ArcMirror.js";
import { BeamSplitterElement } from "../model/mirrors/BeamSplitterElement.js";
import { IdealCurvedMirror } from "../model/mirrors/IdealCurvedMirror.js";
import { ParabolicMirror } from "../model/mirrors/ParabolicMirror.js";
import { SegmentMirror } from "../model/mirrors/SegmentMirror.js";
import type { OpticalElement } from "../model/optics/OpticsTypes.js";
import { CircleGlassView } from "./glass/CircleGlassView.js";
import { HalfPlaneGlassView } from "./glass/HalfPlaneGlassView.js";
import { IdealLensView } from "./glass/IdealLensView.js";
import { PolygonGlassView } from "./glass/PolygonGlassView.js";
import { ArcMirrorView } from "./mirrors/ArcMirrorView.js";
import { BeamSplitterView } from "./mirrors/BeamSplitterView.js";
import { IdealCurvedMirrorView } from "./mirrors/IdealCurvedMirrorView.js";
import { ParabolicMirrorView } from "./mirrors/ParabolicMirrorView.js";
import { SegmentMirrorView } from "./mirrors/SegmentMirrorView.js";

/**
* Create and return a Scenery Node that visually represents the given
* optical element. Returns null if the element type has no view (e.g.
* light sources and blockers are handled separately).
*/
export function createOpticalElementView(element: OpticalElement): Node | null {
// ── Mirrors ───────────────────────────────────────────────────────────────
if (element instanceof SegmentMirror) {
return new SegmentMirrorView(element);
}
if (element instanceof ArcMirror) {
return new ArcMirrorView(element);
}
if (element instanceof ParabolicMirror) {
return new ParabolicMirrorView(element);
}
if (element instanceof IdealCurvedMirror) {
return new IdealCurvedMirrorView(element);
}
if (element instanceof BeamSplitterElement) {
return new BeamSplitterView(element);
}

// ── Glass / Lenses ────────────────────────────────────────────────────────
if (element instanceof IdealLens) {
return new IdealLensView(element);
}
if (element instanceof CircleGlass) {
return new CircleGlassView(element);
}
// PolygonGlass must come after IdealLens / CircleGlass (no overlap), but
// SphericalLens extends PolygonGlass so this branch handles both.
if (element instanceof PolygonGlass) {
return new PolygonGlassView(element);
}
if (element instanceof HalfPlaneGlass) {
return new HalfPlaneGlassView(element);
}

// Light sources and blockers are not handled here.
return null;
}

opticsLab.register("createOpticalElementView", createOpticalElementView);
43 changes: 18 additions & 25 deletions src/rayTracing/view/SimScreenView.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import { Rectangle, Text } from "scenerystack/scenery";
import { Node } from "scenerystack/scenery";
import { ResetAllButton } from "scenerystack/scenery-phet";
import { ScreenView, type ScreenViewOptions } from "scenerystack/sim";
import { RESET_BUTTON_MARGIN } from "../../OpticsLabConstants.js";
import opticsLab from "../../OpticsLabNamespace.js";
import type { OpticsLabPreferencesModel } from "../../preferences/OpticsLabPreferencesModel.js";
import type { SimModel } from "../model/SimModel.js";
import { createOpticalElementView } from "./OpticalElementViewFactory.js";

export class SimScreenView extends ScreenView {
private readonly rotatingRectangle: Rectangle;
private readonly opticsLabPreferences: OpticsLabPreferencesModel;

public constructor(model: SimModel, opticsLabPreferences: OpticsLabPreferencesModel, options?: ScreenViewOptions) {
public constructor(model: SimModel, _opticsLabPreferences: OpticsLabPreferencesModel, options?: ScreenViewOptions) {
super(options);
this.opticsLabPreferences = opticsLabPreferences;

const tandem = options?.tandem;

// Sample Content

this.rotatingRectangle = new Rectangle(-150, -20, 300, 40, {
fill: "#ccc",
translation: this.layoutBounds.center,
...(tandem && { tandem: tandem.createTandem("rotatingRectangle") }),
// ── Optical Elements Layer ──────────────────────────────────────────────
// Create a Scenery node for every mirror and glass/lens in the scene.
const elementsLayer = new Node({
...(tandem && { tandem: tandem.createTandem("elementsLayer") }),
});
this.addChild(this.rotatingRectangle);

this.addChild(
new Text("Content goes here", {
font: "24px sans-serif",
center: this.layoutBounds.center,
...(tandem && { tandem: tandem.createTandem("contentText") }),
}),
);
for (const element of model.scene.getAllElements()) {
const elementView = createOpticalElementView(element);
if (elementView) {
elementsLayer.addChild(elementView);
}
}

this.addChild(elementsLayer);

// ── Reset Button ────────────────────────────────────────────────────────
const resetAllButton = new ResetAllButton({
listener: () => {
model.reset();
Expand All @@ -49,11 +45,8 @@ export class SimScreenView extends ScreenView {
// Called when the user presses the reset-all button
}

public step(dt: number): void {
// Called every frame, with the time since the last frame in seconds
if (this.opticsLabPreferences.enableDemoAnimationProperty.value) {
this.rotatingRectangle.rotation += 2 * dt;
}
public override step(_dt: number): void {
// Animation step — hook available for future per-frame updates
}
}

Expand Down
Loading