diff --git a/src/rayTracing/model/light-sources/ArcLightSource.ts b/src/rayTracing/model/light-sources/ArcLightSource.ts index 74ffda0..2e709cf 100644 --- a/src/rayTracing/model/light-sources/ArcLightSource.ts +++ b/src/rayTracing/model/light-sources/ArcLightSource.ts @@ -39,7 +39,7 @@ export class ArcLightSource extends BaseElement { public constructor( position: Point, direction = 0, - emissionAngle = Math.PI/3, // 60 degrees + emissionAngle = Math.PI / 3, // 60 degrees brightness = 0.5, wavelength = GREEN_WAVELENGTH, ) { diff --git a/src/rayTracing/view/ComponentCarousel.ts b/src/rayTracing/view/ComponentCarousel.ts index a1dc76d..3094e5e 100644 --- a/src/rayTracing/view/ComponentCarousel.ts +++ b/src/rayTracing/view/ComponentCarousel.ts @@ -9,7 +9,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Circle, Node, Path, type PressListenerEvent, RichDragListener, Text } from "scenerystack/scenery"; import { Carousel, type CarouselItem } from "scenerystack/sun"; import opticsLab from "../../OpticsLabNamespace.js"; @@ -17,9 +17,9 @@ import { ApertureElement } from "../model/blockers/ApertureElement.js"; import { CircleBlocker } from "../model/blockers/CircleBlocker.js"; import { LineBlocker } from "../model/blockers/LineBlocker.js"; import { CircleGlass } from "../model/glass/CircleGlass.js"; +import { Glass } from "../model/glass/Glass.js"; import { HalfPlaneGlass } from "../model/glass/HalfPlaneGlass.js"; import { IdealLens } from "../model/glass/IdealLens.js"; -import { Glass } from "../model/glass/Glass.js"; import { SphericalLens } from "../model/glass/SphericalLens.js"; import { ArcLightSource } from "../model/light-sources/ArcLightSource.js"; import { BeamSource } from "../model/light-sources/BeamSource.js"; @@ -79,7 +79,9 @@ function arcSourceIcon(): Node { .moveTo(0, 0) .arc(0, 0, 14, -Math.PI * 0.3, Math.PI * 0.3, false) .close(); - node.addChild(new Path(sectorShape, { fill: "rgba(255, 215, 60, 0.18)", stroke: "rgba(255, 215, 60, 0.65)", lineWidth: 1.2 })); + node.addChild( + new Path(sectorShape, { fill: "rgba(255, 215, 60, 0.18)", stroke: "rgba(255, 215, 60, 0.65)", lineWidth: 1.2 }), + ); const spokeShape = new Shape(); for (let i = -2; i <= 2; i++) { const a = (i / 2) * Math.PI * 0.55; @@ -87,7 +89,9 @@ function arcSourceIcon(): Node { spokeShape.lineTo(Math.cos(a) * 12, Math.sin(a) * 12); } node.addChild(new Path(spokeShape, { stroke: "rgba(255, 210, 60, 0.60)", lineWidth: 1 })); - node.addChild(new Circle(4, { fill: "rgba(255, 220, 80, 0.35)", stroke: "rgba(255, 220, 80, 0.90)", lineWidth: 1.5 })); + node.addChild( + new Circle(4, { fill: "rgba(255, 220, 80, 0.35)", stroke: "rgba(255, 220, 80, 0.90)", lineWidth: 1.5 }), + ); return node; } @@ -96,7 +100,10 @@ function beamSourceIcon(): Node { for (let dy = -8; dy <= 8; dy += 8) { const shape = new Shape().moveTo(-12, dy).lineTo(8, dy); node.addChild(new Path(shape, { stroke: "#44ee66", lineWidth: 1.5 })); - const arrow = new Shape().moveTo(5, dy - 4).lineTo(12, dy).lineTo(5, dy + 4); + const arrow = new Shape() + .moveTo(5, dy - 4) + .lineTo(12, dy) + .lineTo(5, dy + 4); node.addChild(new Path(arrow, { stroke: "#44ee66", lineWidth: 1.5, lineCap: "round", lineJoin: "round" })); } return node; @@ -135,7 +142,11 @@ function parabolicMirrorIcon(): Node { const t = (i / N) * 2 - 1; const x = t * 14; const y = -t * t * 10 + 4; - if (i === 0) { shape.moveTo(x, y); } else { shape.lineTo(x, y); } + if (i === 0) { + shape.moveTo(x, y); + } else { + shape.lineTo(x, y); + } } node.addChild(new Path(shape, { stroke: "#666", lineWidth: 4, lineCap: "round", lineJoin: "round" })); node.addChild(new Path(shape, { stroke: "#d8d8d8", lineWidth: 2, lineCap: "round", lineJoin: "round" })); @@ -173,7 +184,9 @@ function idealLensIcon(): Node { function circleGlassIcon(): Node { const node = new Node(); - node.addChild(new Circle(12, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 })); + node.addChild( + new Circle(12, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 }), + ); return node; } @@ -183,14 +196,18 @@ function sphericalLensIcon(): Node { .arc(-12, 0, 18, -Math.PI / 4, Math.PI / 4) .arc(12, 0, 18, Math.PI - Math.PI / 4, Math.PI + Math.PI / 4) .close(); - node.addChild(new Path(shape, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 })); + node.addChild( + new Path(shape, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 }), + ); return node; } function polygonGlassIcon(): Node { const node = new Node(); const shape = new Shape().moveTo(0, -12).lineTo(12, 10).lineTo(-12, 10).close(); - node.addChild(new Path(shape, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 })); + node.addChild( + new Path(shape, { fill: "rgba(100, 180, 255, 0.25)", stroke: "rgba(60, 130, 210, 0.8)", lineWidth: 1.5 }), + ); return node; } diff --git a/src/rayTracing/view/EditContainerNode.ts b/src/rayTracing/view/EditContainerNode.ts index 5e601a4..da6cfde 100644 --- a/src/rayTracing/view/EditContainerNode.ts +++ b/src/rayTracing/view/EditContainerNode.ts @@ -13,21 +13,21 @@ * SimScreenView can supply the callback after selection is triggered. */ +import { NumberProperty, type Property } from "scenerystack/axon"; +import { type Bounds2, Dimension2, Range } from "scenerystack/dot"; import { HBox, Node, Text, VBox } from "scenerystack/scenery"; import { HSlider, Panel, TextPushButton } from "scenerystack/sun"; -import { NumberProperty, Property } from "scenerystack/axon"; -import { Bounds2, Dimension2, Range } from "scenerystack/dot"; import { Tandem } from "scenerystack/tandem"; import opticsLab from "../../OpticsLabNamespace.js"; -import { SphericalLens } from "../model/glass/SphericalLens.js"; -import { IdealLens } from "../model/glass/IdealLens.js"; import { BaseGlass } from "../model/glass/BaseGlass.js"; +import { IdealLens } from "../model/glass/IdealLens.js"; +import { SphericalLens } from "../model/glass/SphericalLens.js"; import { ArcLightSource } from "../model/light-sources/ArcLightSource.js"; import { BeamSource } from "../model/light-sources/BeamSource.js"; import { PointSourceElement } from "../model/light-sources/PointSourceElement.js"; import { SingleRaySource } from "../model/light-sources/SingleRaySource.js"; -import { IdealCurvedMirror } from "../model/mirrors/IdealCurvedMirror.js"; import { BeamSplitterElement } from "../model/mirrors/BeamSplitterElement.js"; +import { IdealCurvedMirror } from "../model/mirrors/IdealCurvedMirror.js"; import type { OpticalElement } from "../model/optics/OpticsTypes.js"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -109,10 +109,7 @@ function makeSlider( return new VBox({ spacing: 2, align: "left", - children: [ - new Text(label, { font: LABEL_FONT, fill: LABEL_FILL }), - slider, - ], + children: [new Text(label, { font: LABEL_FONT, fill: LABEL_FILL }), slider], }); } @@ -155,10 +152,7 @@ export class EditContainerNode extends Node { // ── Private ─────────────────────────────────────────────────────────────── - private _renderFor( - element: OpticalElement | null, - onDelete: (e: OpticalElement) => void, - ): void { + private _renderFor(element: OpticalElement | null, onDelete: (e: OpticalElement) => void): void { this.removeAllChildren(); if (!element) { @@ -221,92 +215,193 @@ export class EditContainerNode extends Node { // ── Light Sources ───────────────────────────────────────────────────── if (element instanceof ArcLightSource) { sliders.push( - makeSlider("Brightness", element.brightness, new Range(0.05, 2), (v) => { - element.brightness = v; - }, triggerRebuild), - makeSlider("Wavelength (nm)", element.wavelength, new Range(380, 780), (v) => { - element.wavelength = v; - }, triggerRebuild), - makeSlider("Emission Angle (°)", element.emissionAngle * (180 / Math.PI), - new Range(5, 360), (v) => { + makeSlider( + "Brightness", + element.brightness, + new Range(0.05, 2), + (v) => { + element.brightness = v; + }, + triggerRebuild, + ), + makeSlider( + "Wavelength (nm)", + element.wavelength, + new Range(380, 780), + (v) => { + element.wavelength = v; + }, + triggerRebuild, + ), + makeSlider( + "Emission Angle (°)", + element.emissionAngle * (180 / Math.PI), + new Range(5, 360), + (v) => { element.emissionAngle = v * (Math.PI / 180); - }, triggerRebuild), + }, + triggerRebuild, + ), ); } else if (element instanceof PointSourceElement) { sliders.push( - makeSlider("Brightness", element.brightness, new Range(0.05, 2), (v) => { - element.brightness = v; - }, triggerRebuild), - makeSlider("Wavelength (nm)", element.wavelength, new Range(380, 780), (v) => { - element.wavelength = v; - }, triggerRebuild), + makeSlider( + "Brightness", + element.brightness, + new Range(0.05, 2), + (v) => { + element.brightness = v; + }, + triggerRebuild, + ), + makeSlider( + "Wavelength (nm)", + element.wavelength, + new Range(380, 780), + (v) => { + element.wavelength = v; + }, + triggerRebuild, + ), ); } else if (element instanceof BeamSource) { sliders.push( - makeSlider("Brightness", element.brightness, new Range(0.05, 2), (v) => { - element.brightness = v; - }, triggerRebuild), - makeSlider("Wavelength (nm)", element.wavelength, new Range(380, 780), (v) => { - element.wavelength = v; - }, triggerRebuild), - makeSlider("Divergence (°)", element.emisAngle, new Range(0, 90), (v) => { - element.emisAngle = v; - }, triggerRebuild), + makeSlider( + "Brightness", + element.brightness, + new Range(0.05, 2), + (v) => { + element.brightness = v; + }, + triggerRebuild, + ), + makeSlider( + "Wavelength (nm)", + element.wavelength, + new Range(380, 780), + (v) => { + element.wavelength = v; + }, + triggerRebuild, + ), + makeSlider( + "Divergence (°)", + element.emisAngle, + new Range(0, 90), + (v) => { + element.emisAngle = v; + }, + triggerRebuild, + ), ); } else if (element instanceof SingleRaySource) { sliders.push( - makeSlider("Brightness", element.brightness, new Range(0.05, 2), (v) => { - element.brightness = v; - }, triggerRebuild), - makeSlider("Wavelength (nm)", element.wavelength, new Range(380, 780), (v) => { - element.wavelength = v; - }, triggerRebuild), + makeSlider( + "Brightness", + element.brightness, + new Range(0.05, 2), + (v) => { + element.brightness = v; + }, + triggerRebuild, + ), + makeSlider( + "Wavelength (nm)", + element.wavelength, + new Range(380, 780), + (v) => { + element.wavelength = v; + }, + triggerRebuild, + ), ); - // ── Glass / Lenses ──────────────────────────────────────────────────── + // ── Glass / Lenses ──────────────────────────────────────────────────── } else if (element instanceof SphericalLens) { const { r1, r2 } = element.getDR1R2(); const R_RANGE = new Range(-2000, 2000); sliders.push( - makeSlider("R₁ (left surface)", safeClamp(r1, R_RANGE.min, R_RANGE.max, 500), R_RANGE, (v) => { - const { d, r2: cr2 } = element.getDR1R2(); - element.createLensWithDR1R2(d, v, cr2); - }, triggerRebuild), - makeSlider("R₂ (right surface)", safeClamp(r2, R_RANGE.min, R_RANGE.max, -500), R_RANGE, (v) => { - const { d, r1: cr1 } = element.getDR1R2(); - element.createLensWithDR1R2(d, cr1, v); - }, triggerRebuild), - makeSlider("Ref. Index", element.refIndex, new Range(1, 3), (v) => { - element.refIndex = v; - }, triggerRebuild), + makeSlider( + "R₁ (left surface)", + safeClamp(r1, R_RANGE.min, R_RANGE.max, 500), + R_RANGE, + (v) => { + const { d, r2: cr2 } = element.getDR1R2(); + element.createLensWithDR1R2(d, v, cr2); + }, + triggerRebuild, + ), + makeSlider( + "R₂ (right surface)", + safeClamp(r2, R_RANGE.min, R_RANGE.max, -500), + R_RANGE, + (v) => { + const { d, r1: cr1 } = element.getDR1R2(); + element.createLensWithDR1R2(d, cr1, v); + }, + triggerRebuild, + ), + makeSlider( + "Ref. Index", + element.refIndex, + new Range(1, 3), + (v) => { + element.refIndex = v; + }, + triggerRebuild, + ), ); } else if (element instanceof IdealLens) { sliders.push( - makeSlider("Focal Length (px)", element.focalLength, new Range(-800, 800), (v) => { - element.focalLength = v; - }, triggerRebuild), + makeSlider( + "Focal Length (px)", + element.focalLength, + new Range(-800, 800), + (v) => { + element.focalLength = v; + }, + triggerRebuild, + ), ); } else if (element instanceof BaseGlass) { // Covers CircleGlass, HalfPlaneGlass, Glass (prism) sliders.push( - makeSlider("Ref. Index", element.refIndex, new Range(1, 3), (v) => { - element.refIndex = v; - }, triggerRebuild), + makeSlider( + "Ref. Index", + element.refIndex, + new Range(1, 3), + (v) => { + element.refIndex = v; + }, + triggerRebuild, + ), ); - // ── Mirrors ─────────────────────────────────────────────────────────── + // ── Mirrors ─────────────────────────────────────────────────────────── } else if (element instanceof IdealCurvedMirror) { sliders.push( - makeSlider("Focal Length (px)", element.focalLength, new Range(10, 800), (v) => { - element.focalLength = v; - }, triggerRebuild), + makeSlider( + "Focal Length (px)", + element.focalLength, + new Range(10, 800), + (v) => { + element.focalLength = v; + }, + triggerRebuild, + ), ); } else if (element instanceof BeamSplitterElement) { sliders.push( - makeSlider("Transmission ratio", element.transRatio, new Range(0, 1), (v) => { - element.transRatio = v; - }, triggerRebuild), + makeSlider( + "Transmission ratio", + element.transRatio, + new Range(0, 1), + (v) => { + element.transRatio = v; + }, + triggerRebuild, + ), ); } diff --git a/src/rayTracing/view/OpticalElementViewFactory.ts b/src/rayTracing/view/OpticalElementViewFactory.ts index 82f8a8d..9c5c148 100644 --- a/src/rayTracing/view/OpticalElementViewFactory.ts +++ b/src/rayTracing/view/OpticalElementViewFactory.ts @@ -5,16 +5,16 @@ * Maps model types to their corresponding view classes. */ +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import type { Node, RichDragListener } from "scenerystack/scenery"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; import opticsLab from "../../OpticsLabNamespace.js"; import { ApertureElement } from "../model/blockers/ApertureElement.js"; import { CircleBlocker } from "../model/blockers/CircleBlocker.js"; import { LineBlocker } from "../model/blockers/LineBlocker.js"; import { CircleGlass } from "../model/glass/CircleGlass.js"; +import { Glass } from "../model/glass/Glass.js"; import { HalfPlaneGlass } from "../model/glass/HalfPlaneGlass.js"; import { IdealLens } from "../model/glass/IdealLens.js"; -import { Glass } from "../model/glass/Glass.js"; import { SphericalLens } from "../model/glass/SphericalLens.js"; import { ArcLightSource } from "../model/light-sources/ArcLightSource.js"; import { BeamSource } from "../model/light-sources/BeamSource.js"; @@ -30,9 +30,9 @@ import { ApertureView } from "./blockers/ApertureView.js"; import { CircleBlockerView } from "./blockers/CircleBlockerView.js"; import { LineBlockerView } from "./blockers/LineBlockerView.js"; import { CircleGlassView } from "./glass/CircleGlassView.js"; +import { GlassView } from "./glass/GlassView.js"; import { HalfPlaneGlassView } from "./glass/HalfPlaneGlassView.js"; import { IdealLensView } from "./glass/IdealLensView.js"; -import { GlassView } from "./glass/GlassView.js"; import { SphericalLensView } from "./glass/SphericalLensView.js"; import { ArcLightSourceView } from "./light-sources/ArcLightSourceView.js"; import { BeamSourceView } from "./light-sources/BeamSourceView.js"; @@ -54,10 +54,7 @@ export type OpticalElementView = Node & { readonly bodyDragListener: RichDragLis * Create and return a Scenery Node that visually represents the given * optical element. Returns null if the element type has no view. */ -export function createOpticalElementView( - element: OpticalElement, - mvt: ModelViewTransform2, -): OpticalElementView | null { +export function createOpticalElementView(element: OpticalElement, mvt: ModelViewTransform2): OpticalElementView | null { // ── Light Sources ───────────────────────────────────────────────────────── if (element instanceof ArcLightSource) { return new ArcLightSourceView(element, mvt); diff --git a/src/rayTracing/view/RayPropagationView.ts b/src/rayTracing/view/RayPropagationView.ts index 5ef40d5..2862704 100644 --- a/src/rayTracing/view/RayPropagationView.ts +++ b/src/rayTracing/view/RayPropagationView.ts @@ -11,7 +11,7 @@ */ import type { Bounds2 } from "scenerystack/dot"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { CanvasNode, type CanvasNodeOptions } from "scenerystack/scenery"; import opticsLab from "../../OpticsLabNamespace.js"; import type { TracedSegment } from "../model/optics/RayTracer.js"; diff --git a/src/rayTracing/view/SimScreenView.ts b/src/rayTracing/view/SimScreenView.ts index 4ff18b4..3f2a0eb 100644 --- a/src/rayTracing/view/SimScreenView.ts +++ b/src/rayTracing/view/SimScreenView.ts @@ -1,14 +1,14 @@ +import { Property } from "scenerystack/axon"; import { Vector2 } from "scenerystack/dot"; +import { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Node } from "scenerystack/scenery"; import { ResetAllButton } from "scenerystack/scenery-phet"; import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; -import { Property } from "scenerystack/axon"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; 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 type { OpticalElement } from "../model/optics/OpticsTypes.js"; +import type { SimModel } from "../model/SimModel.js"; import { createComponentCarousel } from "./ComponentCarousel.js"; import { EditContainerNode } from "./EditContainerNode.js"; import { createOpticalElementView, type OpticalElementView } from "./OpticalElementViewFactory.js"; @@ -91,11 +91,7 @@ export class SimScreenView extends ScreenView { model.scene.removeElement(element.id); }; - this.editContainerNode = new EditContainerNode( - this.selectedElementProperty, - onDelete, - this.layoutBounds, - ); + this.editContainerNode = new EditContainerNode(this.selectedElementProperty, onDelete, this.layoutBounds); this.addChild(this.editContainerNode); // ── Component Carousel (toolbox) ───────────────────────────────────────── diff --git a/src/rayTracing/view/ViewHelpers.ts b/src/rayTracing/view/ViewHelpers.ts index 579ed11..32ed6a1 100644 --- a/src/rayTracing/view/ViewHelpers.ts +++ b/src/rayTracing/view/ViewHelpers.ts @@ -13,7 +13,7 @@ * that `listener.modelDelta` is already in model units. */ -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Circle, type Node, RichDragListener } from "scenerystack/scenery"; import { Tandem } from "scenerystack/tandem"; import opticsLab from "../../OpticsLabNamespace.js"; diff --git a/src/rayTracing/view/blockers/ApertureView.ts b/src/rayTracing/view/blockers/ApertureView.ts index fc61e22..e93397d 100644 --- a/src/rayTracing/view/blockers/ApertureView.ts +++ b/src/rayTracing/view/blockers/ApertureView.ts @@ -6,7 +6,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { ApertureElement } from "../../model/blockers/ApertureElement.js"; @@ -26,7 +26,10 @@ export class ApertureView extends Node { private readonly handle3: Circle; private readonly handle4: Circle; - public constructor(private readonly aperture: ApertureElement, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly aperture: ApertureElement, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { diff --git a/src/rayTracing/view/blockers/CircleBlockerView.ts b/src/rayTracing/view/blockers/CircleBlockerView.ts index 16a1c44..4312ff6 100644 --- a/src/rayTracing/view/blockers/CircleBlockerView.ts +++ b/src/rayTracing/view/blockers/CircleBlockerView.ts @@ -6,7 +6,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { CircleBlocker } from "../../model/blockers/CircleBlocker.js"; @@ -22,7 +22,10 @@ export class CircleBlockerView extends Node { private readonly handleCenter: Circle; private readonly handleBoundary: Circle; - public constructor(private readonly blocker: CircleBlocker, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly blocker: CircleBlocker, + private readonly mvt: ModelViewTransform2, + ) { super(); this.circlePath = new Path(null, { diff --git a/src/rayTracing/view/blockers/LineBlockerView.ts b/src/rayTracing/view/blockers/LineBlockerView.ts index 57dc2fa..374d0de 100644 --- a/src/rayTracing/view/blockers/LineBlockerView.ts +++ b/src/rayTracing/view/blockers/LineBlockerView.ts @@ -6,7 +6,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { LineBlocker } from "../../model/blockers/LineBlocker.js"; @@ -24,7 +24,10 @@ export class LineBlockerView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly blocker: LineBlocker, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly blocker: LineBlocker, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { diff --git a/src/rayTracing/view/glass/CircleGlassView.ts b/src/rayTracing/view/glass/CircleGlassView.ts index 3001e22..affadc6 100644 --- a/src/rayTracing/view/glass/CircleGlassView.ts +++ b/src/rayTracing/view/glass/CircleGlassView.ts @@ -7,7 +7,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { CircleGlass } from "../../model/glass/CircleGlass.js"; @@ -24,7 +24,10 @@ export class CircleGlassView extends Node { private readonly handleCenter: Circle; private readonly handleBoundary: Circle; - public constructor(private readonly glass: CircleGlass, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly glass: CircleGlass, + private readonly mvt: ModelViewTransform2, + ) { super(); this.circlePath = new Path(null, { diff --git a/src/rayTracing/view/glass/GlassView.ts b/src/rayTracing/view/glass/GlassView.ts index 18b6069..fde94ac 100644 --- a/src/rayTracing/view/glass/GlassView.ts +++ b/src/rayTracing/view/glass/GlassView.ts @@ -9,7 +9,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { Glass, GlassPathPoint } from "../../model/glass/Glass.js"; @@ -75,9 +75,14 @@ export class GlassView extends Node { v.y = p.y; }, })); - this.bodyDragListener = attachTranslationDrag(this.glassPath, allVertPoints, () => { - this.rebuild(); - }, mvt); + this.bodyDragListener = attachTranslationDrag( + this.glassPath, + allVertPoints, + () => { + this.rebuild(); + }, + mvt, + ); } protected rebuild(): void { @@ -128,7 +133,17 @@ export class GlassView extends Node { const a1 = Math.atan2(p1.y - center.y, p1.x - center.x); const a2 = Math.atan2(p2.y - center.y, p2.x - center.x); const a3 = Math.atan2(p3.y - center.y, p3.x - center.x); - const acw = (a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2); + + // In canvas (y-down) the angle of a model point is -a_model. Going + // clockwise in canvas (anticlockwise=false) means increasing canvas angle. + // The clockwise canvas distance from -a1 to an angle -aX equals + // (a1 - aX + 2π) % 2π. The arc reaches the control point via the short + // (clockwise) route when that distance is less than the distance to the + // end point; otherwise the counterclockwise route passes through it. + const tau = 2 * Math.PI; + const cwCanvas1ToCtrl = (((a1 - a3) % tau) + tau) % tau; + const cwCanvas1ToEnd = (((a1 - a2) % tau) + tau) % tau; + const acw = cwCanvas1ToCtrl >= cwCanvas1ToEnd; // Convert center and radius to view space; negate angles for y-inversion const vcx = this.mvt.modelToViewX(center.x); diff --git a/src/rayTracing/view/glass/HalfPlaneGlassView.ts b/src/rayTracing/view/glass/HalfPlaneGlassView.ts index 5aa5857..b314b78 100644 --- a/src/rayTracing/view/glass/HalfPlaneGlassView.ts +++ b/src/rayTracing/view/glass/HalfPlaneGlassView.ts @@ -8,7 +8,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { HalfPlaneGlass } from "../../model/glass/HalfPlaneGlass.js"; @@ -19,7 +19,7 @@ const BORDER_STROKE = "rgba(60, 130, 210, 0.9)"; const BORDER_WIDTH = 2; const HATCH_STROKE = "rgba(100, 180, 255, 0.45)"; const HATCH_WIDTH = 1; -const HATCH_SPACING = 0.20; // metres between hatching lines +const HATCH_SPACING = 0.2; // metres between hatching lines const HATCH_DEPTH = 0.18; // how far into the glass the hatching goes (metres) const HATCH_COUNT = 8; // maximum number of hatch lines @@ -30,7 +30,10 @@ export class HalfPlaneGlassView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly glass: HalfPlaneGlass, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly glass: HalfPlaneGlass, + private readonly mvt: ModelViewTransform2, + ) { super(); this.borderPath = new Path(null, { diff --git a/src/rayTracing/view/glass/IdealLensView.ts b/src/rayTracing/view/glass/IdealLensView.ts index 5bd1259..280c7d2 100644 --- a/src/rayTracing/view/glass/IdealLensView.ts +++ b/src/rayTracing/view/glass/IdealLensView.ts @@ -8,7 +8,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { IdealLens } from "../../model/glass/IdealLens.js"; @@ -17,7 +17,7 @@ import { attachEndpointDrag, attachTranslationDrag, createHandle } from "../View // ── Styling constants ───────────────────────────────────────────────────────── const LENS_STROKE = "#44cc88"; const LENS_WIDTH = 3; -const ARROW_SIZE = 0.10; // metres (was 10px) — half-length of each arrow head arm +const ARROW_SIZE = 0.1; // metres (was 10px) — half-length of each arrow head arm export class IdealLensView extends Node { public readonly bodyDragListener: RichDragListener; @@ -26,7 +26,10 @@ export class IdealLensView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly lens: IdealLens, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly lens: IdealLens, + private readonly mvt: ModelViewTransform2, + ) { super(); this.linePath = new Path(null, { diff --git a/src/rayTracing/view/glass/SphericalLensView.ts b/src/rayTracing/view/glass/SphericalLensView.ts index be91e86..794d4eb 100644 --- a/src/rayTracing/view/glass/SphericalLensView.ts +++ b/src/rayTracing/view/glass/SphericalLensView.ts @@ -9,7 +9,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Path } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { GlassPathPoint } from "../../model/glass/Glass.js"; @@ -31,7 +31,10 @@ export class SphericalLensView extends GlassView { private readonly focalFront: Path; private readonly focalBack: Path; - public constructor(private readonly lens: SphericalLens, mvt: ModelViewTransform2) { + public constructor( + private readonly lens: SphericalLens, + mvt: ModelViewTransform2, + ) { super(lens, mvt, getHandleVerts(lens)); this.focalFront = new Path(null, { fill: FOCAL_FILL }); diff --git a/src/rayTracing/view/light-sources/ArcLightSourceView.ts b/src/rayTracing/view/light-sources/ArcLightSourceView.ts index 2f7267d..93535d2 100644 --- a/src/rayTracing/view/light-sources/ArcLightSourceView.ts +++ b/src/rayTracing/view/light-sources/ArcLightSourceView.ts @@ -8,7 +8,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, RichDragListener, type RichDragListenerOptions } from "scenerystack/scenery"; import { Tandem } from "scenerystack/tandem"; import opticsLab from "../../../OpticsLabNamespace.js"; @@ -18,9 +18,9 @@ import { attachTranslationDrag, createHandle } from "../ViewHelpers.js"; // ── Visual constants ────────────────────────────────────────────────────────── /** Rim radius in model metres (0.40 m = 40 px at 100 px/m). */ -const RIM_RADIUS = 0.40; +const RIM_RADIUS = 0.4; -const GLOW_RADIUS = 12; // px – fixed visual size +const GLOW_RADIUS = 12; // px – fixed visual size const GLOW_FILL = "rgba(255, 220, 80, 0.28)"; const GLOW_STROKE = "rgba(255, 220, 80, 0.90)"; const GLOW_STROKE_WIDTH = 2; @@ -45,8 +45,12 @@ const SECTOR_SAMPLE_N = 48; // ── Helper: build an arc (polyline) by sampling in model space ─────────────── function arcPoints( - cx: number, cy: number, r: number, - startAngle: number, endAngle: number, n: number, + cx: number, + cy: number, + r: number, + startAngle: number, + endAngle: number, + n: number, ): Array<{ x: number; y: number }> { const pts: Array<{ x: number; y: number }> = []; for (let i = 0; i <= n; i++) { @@ -97,14 +101,22 @@ export class ArcLightSourceView extends Node { private readonly dirHandle: Circle; private readonly spreadHandle: Circle; - public constructor(private readonly source: ArcLightSource, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly source: ArcLightSource, + private readonly mvt: ModelViewTransform2, + ) { super(); this.sectorPath = new Path(null, { fill: SECTOR_FILL, stroke: SECTOR_STROKE, lineWidth: SECTOR_LINE_WIDTH }); this.rimPath = new Path(null, { stroke: RIM_STROKE, lineWidth: RIM_LINE_WIDTH }); this.spokePath = new Path(null, { stroke: SPOKE_STROKE, lineWidth: SPOKE_LINE_WIDTH }); this.boundaryPath = new Path(null, { stroke: BOUNDARY_STROKE, lineWidth: BOUNDARY_LINE_WIDTH, lineCap: "round" }); - this.glowPath = new Path(null, { fill: GLOW_FILL, stroke: GLOW_STROKE, lineWidth: GLOW_STROKE_WIDTH, cursor: "grab" }); + this.glowPath = new Path(null, { + fill: GLOW_FILL, + stroke: GLOW_STROKE, + lineWidth: GLOW_STROKE_WIDTH, + cursor: "grab", + }); this.dirHandle = createHandle(this.dirHandlePos(), mvt); this.spreadHandle = createHandle(this.spreadHandlePos(), mvt); @@ -121,21 +133,36 @@ export class ArcLightSourceView extends Node { this.bodyDragListener = attachTranslationDrag( this.glowPath, - [{ get: () => source.position, set: (p) => { source.position = p; } }], - () => { this.rebuild(); }, + [ + { + get: () => source.position, + set: (p) => { + source.position = p; + }, + }, + ], + () => { + this.rebuild(); + }, mvt, ); attachCircleDrag( - this.dirHandle, source, + this.dirHandle, + source, () => source.direction, - (newAlpha) => { source.direction = newAlpha; }, - () => { this.rebuild(); }, + (newAlpha) => { + source.direction = newAlpha; + }, + () => { + this.rebuild(); + }, mvt, ); attachCircleDrag( - this.spreadHandle, source, + this.spreadHandle, + source, () => source.direction + source.emissionAngle / 2, (newHandleAngle) => { let diff = newHandleAngle - source.direction; @@ -143,7 +170,9 @@ export class ArcLightSourceView extends Node { const halfBeta = Math.min(diff, 2 * Math.PI - diff); source.emissionAngle = Math.max(0.01, halfBeta * 2); }, - () => { this.rebuild(); }, + () => { + this.rebuild(); + }, mvt, ); } @@ -165,7 +194,11 @@ export class ArcLightSourceView extends Node { public rebuild(): void { const mvt = this.mvt; - const { position: { x, y }, direction: alpha, emissionAngle: beta } = this.source; + const { + position: { x, y }, + direction: alpha, + emissionAngle: beta, + } = this.source; const halfBeta = beta / 2; const startAngle = alpha - halfBeta; const endAngle = alpha + halfBeta; @@ -194,11 +227,15 @@ export class ArcLightSourceView extends Node { if (!isFullCircle) { const bShape = new Shape() .moveTo(vcx, vcy) - .lineTo(mvt.modelToViewX(x + Math.cos(startAngle) * RIM_RADIUS), - mvt.modelToViewY(y + Math.sin(startAngle) * RIM_RADIUS)) + .lineTo( + mvt.modelToViewX(x + Math.cos(startAngle) * RIM_RADIUS), + mvt.modelToViewY(y + Math.sin(startAngle) * RIM_RADIUS), + ) .moveTo(vcx, vcy) - .lineTo(mvt.modelToViewX(x + Math.cos(endAngle) * RIM_RADIUS), - mvt.modelToViewY(y + Math.sin(endAngle) * RIM_RADIUS)); + .lineTo( + mvt.modelToViewX(x + Math.cos(endAngle) * RIM_RADIUS), + mvt.modelToViewY(y + Math.sin(endAngle) * RIM_RADIUS), + ); this.boundaryPath.shape = bShape; } else { this.boundaryPath.shape = null; @@ -210,12 +247,14 @@ export class ArcLightSourceView extends Node { const step = beta / numSpokes; const scale = Math.abs(mvt.modelToViewDeltaX(1)); // px/m = 100 const innerR = GLOW_RADIUS / scale; // model metres for GLOW_RADIUS px - const outerR = (vRim - 4) / scale; // model metres for (vRim-4) px + const outerR = (vRim - 4) / scale; // model metres for (vRim-4) px const spokeShape = new Shape(); for (let i = 0; i <= numSpokes; i++) { const a = startAngle + i * step; - const imx = x + Math.cos(a) * innerR, imy = y + Math.sin(a) * innerR; - const omx = x + Math.cos(a) * outerR, omy = y + Math.sin(a) * outerR; + const imx = x + Math.cos(a) * innerR, + imy = y + Math.sin(a) * innerR; + const omx = x + Math.cos(a) * outerR, + omy = y + Math.sin(a) * outerR; spokeShape.moveTo(mvt.modelToViewX(imx), mvt.modelToViewY(imy)); spokeShape.lineTo(mvt.modelToViewX(omx), mvt.modelToViewY(omy)); } @@ -226,10 +265,12 @@ export class ArcLightSourceView extends Node { // ── Handles ───────────────────────────────────────────────────────────── const dp = this.dirHandlePos(); - this.dirHandle.x = mvt.modelToViewX(dp.x); this.dirHandle.y = mvt.modelToViewY(dp.y); + this.dirHandle.x = mvt.modelToViewX(dp.x); + this.dirHandle.y = mvt.modelToViewY(dp.y); const sp = this.spreadHandlePos(); - this.spreadHandle.x = mvt.modelToViewX(sp.x); this.spreadHandle.y = mvt.modelToViewY(sp.y); + this.spreadHandle.x = mvt.modelToViewX(sp.x); + this.spreadHandle.y = mvt.modelToViewY(sp.y); } } diff --git a/src/rayTracing/view/light-sources/BeamSourceView.ts b/src/rayTracing/view/light-sources/BeamSourceView.ts index 24ad9ab..8ea18f2 100644 --- a/src/rayTracing/view/light-sources/BeamSourceView.ts +++ b/src/rayTracing/view/light-sources/BeamSourceView.ts @@ -4,7 +4,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { BeamSource } from "../../model/light-sources/BeamSource.js"; @@ -20,9 +20,9 @@ const ARM_STROKE = "rgba(255, 210, 60, 0.55)"; const ARM_LINE_WIDTH = 1; // Model-space distances (metres) -const DIV_ARM_LENGTH = 0.80; // m (was 80 px) -const BRIGHTNESS_ARM_MIN = 0.20; // m (was 20 px) -const BRIGHTNESS_ARM_MAX = 0.72; // m (was 72 px) +const DIV_ARM_LENGTH = 0.8; // m (was 80 px) +const BRIGHTNESS_ARM_MIN = 0.2; // m (was 20 px) +const BRIGHTNESS_ARM_MAX = 0.72; // m (was 72 px) const EMIS_HANDLE_NORMAL_DIST = 0.55; // m (was 55 px) function brightnessToArmLen(b: number): number { @@ -45,10 +45,18 @@ export class BeamSourceView extends Node { private readonly handleBrightness: Circle; private readonly handleEmis: Circle; - public constructor(private readonly source: BeamSource, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly source: BeamSource, + private readonly mvt: ModelViewTransform2, + ) { super(); - this.shieldPath = new Path(null, { stroke: SHIELD_STROKE, lineWidth: SHIELD_WIDTH, lineCap: "round", cursor: "grab" }); + this.shieldPath = new Path(null, { + stroke: SHIELD_STROKE, + lineWidth: SHIELD_WIDTH, + lineCap: "round", + cursor: "grab", + }); this.beamPath = new Path(null, { stroke: BEAM_STROKE, lineWidth: BEAM_WIDTH, lineCap: "round", cursor: "grab" }); this.divPath = new Path(null, { stroke: DIV_STROKE, lineWidth: DIV_LINE_WIDTH }); this.brightnessArmPath = new Path(null, { stroke: ARM_STROKE, lineWidth: ARM_LINE_WIDTH }); @@ -72,15 +80,43 @@ export class BeamSourceView extends Node { this.rebuild(); const translationPoints = [ - { get: () => source.p1, set: (p: { x: number; y: number }) => { source.p1 = p; } }, - { get: () => source.p2, set: (p: { x: number; y: number }) => { source.p2 = p; } }, + { + get: () => source.p1, + set: (p: { x: number; y: number }) => { + source.p1 = p; + }, + }, + { + get: () => source.p2, + set: (p: { x: number; y: number }) => { + source.p2 = p; + }, + }, ] as const; - const rebuild = () => { this.rebuild(); }; + const rebuild = () => { + this.rebuild(); + }; this.bodyDragListener = attachTranslationDrag(this.shieldPath, translationPoints, rebuild, mvt); - attachEndpointDrag(this.handle1, () => source.p1, (p) => { source.p1 = p; }, rebuild, mvt); - attachEndpointDrag(this.handle2, () => source.p2, (p) => { source.p2 = p; }, rebuild, mvt); + attachEndpointDrag( + this.handle1, + () => source.p1, + (p) => { + source.p1 = p; + }, + rebuild, + mvt, + ); + attachEndpointDrag( + this.handle2, + () => source.p2, + (p) => { + source.p2 = p; + }, + rebuild, + mvt, + ); attachEndpointDrag( this.handleBrightness, @@ -110,9 +146,14 @@ export class BeamSourceView extends Node { ); } - private segmentGeometry(): { mid: { x: number; y: number }; normal: { x: number; y: number }; along: { x: number; y: number } } { + private segmentGeometry(): { + mid: { x: number; y: number }; + normal: { x: number; y: number }; + along: { x: number; y: number }; + } { const { p1, p2 } = this.source; - const dx = p2.x - p1.x, dy = p2.y - p1.y; + const dx = p2.x - p1.x, + dy = p2.y - p1.y; const len = Math.sqrt(dx * dx + dy * dy) || 1; const along = { x: dx / len, y: dy / len }; const normal = { x: -along.y, y: along.x }; @@ -141,9 +182,12 @@ export class BeamSourceView extends Node { const { p1, p2, emisAngle } = this.source; const { mid, normal } = this.segmentGeometry(); - const vx1 = mvt.modelToViewX(p1.x), vy1 = mvt.modelToViewY(p1.y); - const vx2 = mvt.modelToViewX(p2.x), vy2 = mvt.modelToViewY(p2.y); - const vmx = mvt.modelToViewX(mid.x), vmy = mvt.modelToViewY(mid.y); + const vx1 = mvt.modelToViewX(p1.x), + vy1 = mvt.modelToViewY(p1.y); + const vx2 = mvt.modelToViewX(p2.x), + vy2 = mvt.modelToViewY(p2.y); + const vmx = mvt.modelToViewX(mid.x), + vmy = mvt.modelToViewY(mid.y); // Aperture line const aperture = new Shape().moveTo(vx1, vy1).lineTo(vx2, vy2); @@ -168,19 +212,25 @@ export class BeamSourceView extends Node { // Brightness arm const bPos = this.computeBrightnessHandlePos(); - const vbx = mvt.modelToViewX(bPos.x), vby = mvt.modelToViewY(bPos.y); + const vbx = mvt.modelToViewX(bPos.x), + vby = mvt.modelToViewY(bPos.y); this.brightnessArmPath.shape = new Shape().moveTo(vmx, vmy).lineTo(vbx, vby); - this.handleBrightness.x = vbx; this.handleBrightness.y = vby; + this.handleBrightness.x = vbx; + this.handleBrightness.y = vby; // Emission-angle arm const ePos = this.computeEmisHandlePos(); - const vex = mvt.modelToViewX(ePos.x), vey = mvt.modelToViewY(ePos.y); + const vex = mvt.modelToViewX(ePos.x), + vey = mvt.modelToViewY(ePos.y); this.emisArmPath.shape = new Shape().moveTo(vmx, vmy).lineTo(vex, vey); - this.handleEmis.x = vex; this.handleEmis.y = vey; + this.handleEmis.x = vex; + this.handleEmis.y = vey; // Endpoint handles - this.handle1.x = vx1; this.handle1.y = vy1; - this.handle2.x = vx2; this.handle2.y = vy2; + this.handle1.x = vx1; + this.handle1.y = vy1; + this.handle2.x = vx2; + this.handle2.y = vy2; } } diff --git a/src/rayTracing/view/light-sources/PointSourceView.ts b/src/rayTracing/view/light-sources/PointSourceView.ts index dd9c57f..245e2b9 100644 --- a/src/rayTracing/view/light-sources/PointSourceView.ts +++ b/src/rayTracing/view/light-sources/PointSourceView.ts @@ -4,13 +4,13 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { PointSourceElement } from "../../model/light-sources/PointSourceElement.js"; import { attachEndpointDrag, attachTranslationDrag, createHandle } from "../ViewHelpers.js"; -const GLOW_RADIUS = 14; // px – fixed visual size +const GLOW_RADIUS = 14; // px – fixed visual size const GLOW_FILL = "rgba(255, 220, 80, 0.28)"; const GLOW_STROKE = "rgba(255, 220, 80, 0.90)"; const GLOW_STROKE_WIDTH = 2; @@ -18,7 +18,7 @@ const GLOW_STROKE_WIDTH = 2; const SPOKE_STROKE = "rgba(255, 210, 60, 0.65)"; const SPOKE_LINE_WIDTH = 1.2; const SPOKE_COUNT = 12; -const SPOKE_OUTER = 26; // px – fixed visual size +const SPOKE_OUTER = 26; // px – fixed visual size const ARM_STROKE = "rgba(255, 210, 60, 0.55)"; const ARM_LINE_WIDTH = 1; @@ -43,10 +43,18 @@ export class PointSourceView extends Node { private readonly armPath: Path; private readonly handleBrightness: Circle; - public constructor(private readonly source: PointSourceElement, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly source: PointSourceElement, + private readonly mvt: ModelViewTransform2, + ) { super(); - this.glowPath = new Path(null, { fill: GLOW_FILL, stroke: GLOW_STROKE, lineWidth: GLOW_STROKE_WIDTH, cursor: "grab" }); + this.glowPath = new Path(null, { + fill: GLOW_FILL, + stroke: GLOW_STROKE, + lineWidth: GLOW_STROKE_WIDTH, + cursor: "grab", + }); this.spokePath = new Path(null, { stroke: SPOKE_STROKE, lineWidth: SPOKE_LINE_WIDTH }); this.armPath = new Path(null, { stroke: ARM_STROKE, lineWidth: ARM_LINE_WIDTH }); this.handleBrightness = createHandle(this.computeBrightnessHandlePos(), mvt); @@ -60,8 +68,17 @@ export class PointSourceView extends Node { this.bodyDragListener = attachTranslationDrag( this.glowPath, - [{ get: () => source.position, set: (p) => { source.position = p; } }], - () => { this.rebuild(); }, + [ + { + get: () => source.position, + set: (p) => { + source.position = p; + }, + }, + ], + () => { + this.rebuild(); + }, mvt, ); @@ -70,10 +87,13 @@ export class PointSourceView extends Node { () => this.computeBrightnessHandlePos(), (newP) => { const { x: cx, y: cy } = source.position; - const dx = newP.x - cx, dy = newP.y - cy; + const dx = newP.x - cx, + dy = newP.y - cy; source.brightness = armLengthToBrightness(Math.sqrt(dx * dx + dy * dy)); }, - () => { this.rebuild(); }, + () => { + this.rebuild(); + }, mvt, ); } @@ -103,7 +123,8 @@ export class PointSourceView extends Node { const outerLen = GLOW_RADIUS + (SPOKE_OUTER - GLOW_RADIUS) * brightness; // Angle in model (y-up) maps to view: we use the same angle but apply y-inversion // by computing model offsets and converting - const cosMv = Math.cos(angle), sinMv = Math.sin(angle); + const cosMv = Math.cos(angle), + sinMv = Math.sin(angle); // inner: GLOW_RADIUS px from centre in view (use view deltas) const vDeltaX = mvt.modelToViewDeltaX(1); // = 100 px/m // For fixed pixel offsets, divide by scale to get model offset, then convert @@ -119,7 +140,8 @@ export class PointSourceView extends Node { // Brightness arm (model-space computation) const hPos = this.computeBrightnessHandlePos(); - const vhx = mvt.modelToViewX(hPos.x), vhy = mvt.modelToViewY(hPos.y); + const vhx = mvt.modelToViewX(hPos.x), + vhy = mvt.modelToViewY(hPos.y); this.armPath.shape = new Shape().moveTo(vcx, vcy).lineTo(vhx, vhy); this.handleBrightness.x = vhx; this.handleBrightness.y = vhy; diff --git a/src/rayTracing/view/light-sources/SingleRaySourceView.ts b/src/rayTracing/view/light-sources/SingleRaySourceView.ts index a171395..a9b4beb 100644 --- a/src/rayTracing/view/light-sources/SingleRaySourceView.ts +++ b/src/rayTracing/view/light-sources/SingleRaySourceView.ts @@ -4,13 +4,13 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { SingleRaySource } from "../../model/light-sources/SingleRaySource.js"; import { attachEndpointDrag, attachTranslationDrag, createHandle } from "../ViewHelpers.js"; -const ORIGIN_RADIUS = 9; // px – fixed visual size +const ORIGIN_RADIUS = 9; // px – fixed visual size const ORIGIN_FILL = "rgba(255, 220, 80, 0.35)"; const ORIGIN_STROKE = "rgba(255, 220, 80, 0.92)"; const ORIGIN_STROKE_WIDTH = 2; @@ -20,7 +20,7 @@ const DIR_LINE_WIDTH = 1.5; const ARROW_STROKE = "rgba(255, 220, 80, 0.90)"; const ARROW_LINE_WIDTH = 1.5; -const ARROW_ARM = 0.10; // m – arrowhead arm length in model space +const ARROW_ARM = 0.1; // m – arrowhead arm length in model space const ARM_STROKE = "rgba(255, 210, 60, 0.55)"; const ARM_LINE_WIDTH = 1; @@ -46,10 +46,18 @@ export class SingleRaySourceView extends Node { private readonly handleDirection: Circle; private readonly handleBrightness: Circle; - public constructor(private readonly source: SingleRaySource, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly source: SingleRaySource, + private readonly mvt: ModelViewTransform2, + ) { super(); - this.originPath = new Path(null, { fill: ORIGIN_FILL, stroke: ORIGIN_STROKE, lineWidth: ORIGIN_STROKE_WIDTH, cursor: "grab" }); + this.originPath = new Path(null, { + fill: ORIGIN_FILL, + stroke: ORIGIN_STROKE, + lineWidth: ORIGIN_STROKE_WIDTH, + cursor: "grab", + }); this.dirPath = new Path(null, { stroke: DIR_STROKE, lineWidth: DIR_LINE_WIDTH }); this.arrowPath = new Path(null, { stroke: ARROW_STROKE, lineWidth: ARROW_LINE_WIDTH, lineCap: "round" }); this.brightnessArmPath = new Path(null, { stroke: ARM_STROKE, lineWidth: ARM_LINE_WIDTH }); @@ -69,18 +77,34 @@ export class SingleRaySourceView extends Node { this.bodyDragListener = attachTranslationDrag( this.originPath, [ - { get: () => source.p1, set: (p) => { source.p1 = p; } }, - { get: () => source.p2, set: (p) => { source.p2 = p; } }, + { + get: () => source.p1, + set: (p) => { + source.p1 = p; + }, + }, + { + get: () => source.p2, + set: (p) => { + source.p2 = p; + }, + }, ], - () => { this.rebuild(); }, + () => { + this.rebuild(); + }, mvt, ); attachEndpointDrag( this.handleDirection, () => source.p2, - (p) => { source.p2 = p; }, - () => { this.rebuild(); }, + (p) => { + source.p2 = p; + }, + () => { + this.rebuild(); + }, mvt, ); @@ -93,14 +117,17 @@ export class SingleRaySourceView extends Node { const proj = (newP.x - p1.x) * perp.x + (newP.y - p1.y) * perp.y; source.brightness = armLenToBrightness(Math.abs(proj)); }, - () => { this.rebuild(); }, + () => { + this.rebuild(); + }, mvt, ); } private rayDir(): { x: number; y: number } { const { p1, p2 } = this.source; - const dx = p2.x - p1.x, dy = p2.y - p1.y; + const dx = p2.x - p1.x, + dy = p2.y - p1.y; const len = Math.sqrt(dx * dx + dy * dy) || 1; return { x: dx / len, y: dy / len }; } @@ -121,8 +148,10 @@ export class SingleRaySourceView extends Node { const mvt = this.mvt; const { p1, p2 } = this.source; - const vx1 = mvt.modelToViewX(p1.x), vy1 = mvt.modelToViewY(p1.y); - const vx2 = mvt.modelToViewX(p2.x), vy2 = mvt.modelToViewY(p2.y); + const vx1 = mvt.modelToViewX(p1.x), + vy1 = mvt.modelToViewY(p1.y); + const vx2 = mvt.modelToViewX(p2.x), + vy2 = mvt.modelToViewY(p2.y); // Origin disc (fixed pixel radius) this.originPath.shape = new Shape().circle(vx1, vy1, ORIGIN_RADIUS); @@ -133,13 +162,15 @@ export class SingleRaySourceView extends Node { // Arrowhead at p2 (ARROW_ARM in model metres) const dir = this.rayDir(); const perp = this.perpUnit(); - const armVx = mvt.modelToViewDeltaX(ARROW_ARM); // px - const armVy = mvt.modelToViewDeltaY(ARROW_ARM); // px (negative because y-inverted) + const armVx = mvt.modelToViewDeltaX(ARROW_ARM); // px + const armVy = mvt.modelToViewDeltaY(ARROW_ARM); // px (negative because y-inverted) const arrowShape = new Shape(); arrowShape .moveTo(vx2, vy2) - .lineTo(vx2 - dir.x * armVx + perp.x * armVx * 0.4, - vy2 + dir.x * armVy * 0 - dir.y * armVy + perp.y * armVy * 0.4); // simplified below + .lineTo( + vx2 - dir.x * armVx + perp.x * armVx * 0.4, + vy2 + dir.x * armVy * 0 - dir.y * armVy + perp.y * armVy * 0.4, + ); // simplified below // Recompute arrow tips using model-space logic then convert const tip1mx = p2.x - dir.x * ARROW_ARM + perp.x * ARROW_ARM * 0.4; const tip1my = p2.y - dir.y * ARROW_ARM + perp.y * ARROW_ARM * 0.4; @@ -152,7 +183,8 @@ export class SingleRaySourceView extends Node { // Brightness arm const bPos = this.computeBrightnessHandlePos(); - const vbx = mvt.modelToViewX(bPos.x), vby = mvt.modelToViewY(bPos.y); + const vbx = mvt.modelToViewX(bPos.x), + vby = mvt.modelToViewY(bPos.y); this.brightnessArmPath.shape = new Shape().moveTo(vx1, vy1).lineTo(vbx, vby); this.handleBrightness.x = vbx; this.handleBrightness.y = vby; diff --git a/src/rayTracing/view/mirrors/ArcMirrorView.ts b/src/rayTracing/view/mirrors/ArcMirrorView.ts index 7c556ff..e457dd6 100644 --- a/src/rayTracing/view/mirrors/ArcMirrorView.ts +++ b/src/rayTracing/view/mirrors/ArcMirrorView.ts @@ -7,7 +7,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { ArcMirror } from "../../model/mirrors/ArcMirror.js"; @@ -119,7 +119,10 @@ export class ArcMirrorView extends Node { private readonly handle2: Circle; private readonly handle3: Circle; - public constructor(private readonly mirror: ArcMirror, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly mirror: ArcMirror, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { diff --git a/src/rayTracing/view/mirrors/BeamSplitterView.ts b/src/rayTracing/view/mirrors/BeamSplitterView.ts index a5c9e9c..d65ffba 100644 --- a/src/rayTracing/view/mirrors/BeamSplitterView.ts +++ b/src/rayTracing/view/mirrors/BeamSplitterView.ts @@ -7,7 +7,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { BeamSplitterElement } from "../../model/mirrors/BeamSplitterElement.js"; @@ -26,7 +26,10 @@ export class BeamSplitterView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly splitter: BeamSplitterElement, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly splitter: BeamSplitterElement, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { diff --git a/src/rayTracing/view/mirrors/IdealCurvedMirrorView.ts b/src/rayTracing/view/mirrors/IdealCurvedMirrorView.ts index cb5d6d7..f6ee83f 100644 --- a/src/rayTracing/view/mirrors/IdealCurvedMirrorView.ts +++ b/src/rayTracing/view/mirrors/IdealCurvedMirrorView.ts @@ -8,7 +8,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { IdealCurvedMirror } from "../../model/mirrors/IdealCurvedMirror.js"; @@ -29,7 +29,10 @@ export class IdealCurvedMirrorView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly mirror: IdealCurvedMirror, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly mirror: IdealCurvedMirror, + private readonly mvt: ModelViewTransform2, + ) { super(); this.linePath = new Path(null, { diff --git a/src/rayTracing/view/mirrors/ParabolicMirrorView.ts b/src/rayTracing/view/mirrors/ParabolicMirrorView.ts index a21355c..4bc0220 100644 --- a/src/rayTracing/view/mirrors/ParabolicMirrorView.ts +++ b/src/rayTracing/view/mirrors/ParabolicMirrorView.ts @@ -7,7 +7,7 @@ */ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { ParabolicMirror } from "../../model/mirrors/ParabolicMirror.js"; @@ -83,7 +83,10 @@ export class ParabolicMirrorView extends Node { private readonly handle2: Circle; private readonly handle3: Circle; - public constructor(private readonly mirror: ParabolicMirror, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly mirror: ParabolicMirror, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { diff --git a/src/rayTracing/view/mirrors/SegmentMirrorView.ts b/src/rayTracing/view/mirrors/SegmentMirrorView.ts index 3089737..0ff269d 100644 --- a/src/rayTracing/view/mirrors/SegmentMirrorView.ts +++ b/src/rayTracing/view/mirrors/SegmentMirrorView.ts @@ -1,5 +1,5 @@ import { Shape } from "scenerystack/kite"; -import { ModelViewTransform2 } from "scenerystack/phetcommon"; +import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { type Circle, Node, Path, type RichDragListener } from "scenerystack/scenery"; import opticsLab from "../../../OpticsLabNamespace.js"; import type { SegmentMirror } from "../../model/mirrors/SegmentMirror.js"; @@ -17,7 +17,10 @@ export class SegmentMirrorView extends Node { private readonly handle1: Circle; private readonly handle2: Circle; - public constructor(private readonly mirror: SegmentMirror, private readonly mvt: ModelViewTransform2) { + public constructor( + private readonly mirror: SegmentMirror, + private readonly mvt: ModelViewTransform2, + ) { super(); this.backPath = new Path(null, { stroke: BACK_STROKE, lineWidth: BACK_WIDTH, lineCap: "round" }); @@ -35,25 +38,61 @@ export class SegmentMirrorView extends Node { this.bodyDragListener = attachTranslationDrag( this.backPath, [ - { get: () => mirror.p1, set: (p) => { mirror.p1 = p; } }, - { get: () => mirror.p2, set: (p) => { mirror.p2 = p; } }, + { + get: () => mirror.p1, + set: (p) => { + mirror.p1 = p; + }, + }, + { + get: () => mirror.p2, + set: (p) => { + mirror.p2 = p; + }, + }, ], - () => { this.rebuild(); }, + () => { + this.rebuild(); + }, + mvt, + ); + attachEndpointDrag( + this.handle1, + () => mirror.p1, + (p) => { + mirror.p1 = p; + }, + () => { + this.rebuild(); + }, + mvt, + ); + attachEndpointDrag( + this.handle2, + () => mirror.p2, + (p) => { + mirror.p2 = p; + }, + () => { + this.rebuild(); + }, mvt, ); - attachEndpointDrag(this.handle1, () => mirror.p1, (p) => { mirror.p1 = p; }, () => { this.rebuild(); }, mvt); - attachEndpointDrag(this.handle2, () => mirror.p2, (p) => { mirror.p2 = p; }, () => { this.rebuild(); }, mvt); } private rebuild(): void { const { p1, p2 } = this.mirror; - const vx1 = this.mvt.modelToViewX(p1.x), vy1 = this.mvt.modelToViewY(p1.y); - const vx2 = this.mvt.modelToViewX(p2.x), vy2 = this.mvt.modelToViewY(p2.y); + const vx1 = this.mvt.modelToViewX(p1.x), + vy1 = this.mvt.modelToViewY(p1.y); + const vx2 = this.mvt.modelToViewX(p2.x), + vy2 = this.mvt.modelToViewY(p2.y); const shape = new Shape().moveTo(vx1, vy1).lineTo(vx2, vy2); this.backPath.shape = shape; this.frontPath.shape = shape; - this.handle1.x = vx1; this.handle1.y = vy1; - this.handle2.x = vx2; this.handle2.y = vy2; + this.handle1.x = vx1; + this.handle1.y = vy1; + this.handle2.x = vx2; + this.handle2.y = vy2; } }