diff --git a/.gitignore b/.gitignore index d90488a..4250a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ coverage/ playwright-report/ test-results/ +# CI security-audit artifact (written by Baton security-audit before lint) +audit.json + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/biome.json b/biome.json index 92cd200..5ce510b 100644 --- a/biome.json +++ b/biome.json @@ -152,7 +152,7 @@ "lineWidth": 120 }, "files": { - "includes": ["**/*.ts", "**/*.js", "**/*.json", "**/*.html", "!dist", "!node_modules", "!.claude"] + "includes": ["**/*.ts", "**/*.js", "**/*.json", "**/*.html", "!dist", "!node_modules", "!.claude", "!audit.json"] }, "overrides": [ { diff --git a/src/OpticsLabConstants.ts b/src/OpticsLabConstants.ts index e899a9d..1cb65d1 100644 --- a/src/OpticsLabConstants.ts +++ b/src/OpticsLabConstants.ts @@ -18,8 +18,10 @@ * 8. Mirror & blocker rendering * 9. Glass / lens rendering * 10. Light-source rendering + * 11+. Grid, gratings, detectors, fiber, carousel, fonts, … */ +import { GREEN_WAVELENGTH } from "./common/model/light-sources/LightSourceConstants.js"; import OpticsLabNamespace from "./OpticsLabNamespace.js"; // ── 1. App chrome ───────────────────────────────────────────────────────────── @@ -79,10 +81,25 @@ export const DEFAULT_FOCAL_LENGTH = 1.0; export const DEFAULT_MAX_RAY_DEPTH = 200; +/** + * Startup default for the `maximumLightRayDepth` query parameter. + * Distinct from `DEFAULT_MAX_RAY_DEPTH` (scene / tracer default when unset). + */ +export const QUERY_DEFAULT_MAX_RAY_DEPTH = 50; + /** Allowed range for `OpticsScene.maxRayDepthProperty` and `maximumLightRayDepth` query parameter (integer). */ export const MAX_RAY_DEPTH_PROPERTY_MIN = 1; export const MAX_RAY_DEPTH_PROPERTY_MAX = 500; +/** + * Length / distance below which a segment or vector is treated as degenerate + * (model metres). Used for near-zero guards across model and view code. + */ +export const LENGTH_DEGENERATE_M = 1e-10; + +/** Maximum number of undo history entries retained by CommandHistory. */ +export const MAX_HISTORY_SIZE = 100; + export const DEFAULT_MIN_BRIGHTNESS = 0.01; export const DEFAULT_RAY_DENSITY = 0.5; export const RAY_DENSITY_MIN = 0.2; @@ -145,6 +162,8 @@ export const PANEL_CONTENT_SPACING = 8; export const BRIGHTNESS_MIN = 0.05; export const BRIGHTNESS_MAX = 2; +/** Step size for brightness NumberControls. */ +export const BRIGHTNESS_CONTROL_DELTA = 0.05; export const WAVELENGTH_MIN_NM = 380; export const WAVELENGTH_MAX_NM = 780; @@ -155,6 +174,13 @@ export const EMISSION_ANGLE_MAX_DEG = 360; export const DIVERGENCE_MIN_DEG = 0; export const DIVERGENCE_MAX_DEG = 90; +/** Default divergence half-angle (degrees) for newly created divergent beams. */ +export const DIVERGENT_BEAM_DEFAULT_EMIS_ANGLE_DEG = 10; + +/** Element rotation / angle NumberControl bounds (degrees). */ +export const ELEMENT_ANGLE_MIN_DEG = 0; +export const ELEMENT_ANGLE_MAX_DEG = 360; + export const SPHERICAL_RADIUS_MIN = -20; export const SPHERICAL_RADIUS_MAX = 20; export const SPHERICAL_R1_FALLBACK = 5; @@ -164,6 +190,23 @@ export const CONSTRAINED_LENS_RADIUS_MIN = 0.5; export const REFRACTIVE_INDEX_MIN = 1; export const REFRACTIVE_INDEX_MAX = 3; +/** Step size for refractive-index NumberControls. */ +export const REFRACTIVE_INDEX_DELTA = 0.05; + +/** Prism size / leg-length NumberControl bounds (model metres). */ +export const PRISM_SIZE_MIN_M = 0.1; +export const PRISM_SIZE_MAX_M = 2.0; +/** Step size for prism size / leg-length NumberControls. */ +export const PRISM_SIZE_DELTA = 0.05; + +/** Dimensional glass (slab / parallelogram / Dove) width bounds (model metres). */ +export const DIMENSIONAL_GLASS_WIDTH_MIN_M = 0.1; +export const DIMENSIONAL_GLASS_WIDTH_MAX_M = 3.0; +/** Dimensional glass height bounds (model metres). */ +export const DIMENSIONAL_GLASS_HEIGHT_MIN_M = 0.1; +export const DIMENSIONAL_GLASS_HEIGHT_MAX_M = 2.0; +/** Step size for dimensional-glass width/height NumberControls. */ +export const DIMENSIONAL_GLASS_SIZE_DELTA = 0.05; export const ARC_MIRROR_RADIUS_MIN = 0.1; export const ARC_MIRROR_RADIUS_MAX = 20; @@ -193,6 +236,16 @@ export const HANDLE_LINE_WIDTH = 1.5; /** Half-width (px) of the invisible filled rectangle used as the drag target for line-segment elements. */ export const LINE_HIT_HALF_WIDTH_PX = 10; +/** Padding (px) around content bounds for the selection rectangle. */ +export const SELECTION_PAD_PX = 8; + +/** Radius (px) of real/virtual image markers in images mode. */ +export const IMAGE_MARKER_RADIUS_PX = HANDLE_RADIUS; +/** Gap (px) between image marker edge and its label. */ +export const IMAGE_LABEL_GAP_PX = 3; +/** Horizontal offset (px) from image marker centre to label. */ +export const IMAGE_LABEL_OFFSET_X_PX = IMAGE_MARKER_RADIUS_PX + IMAGE_LABEL_GAP_PX; + // ── 8. Mirror & blocker rendering ──────────────────────────────────────────── /** Shared back-face stroke width for mirrors and blockers. */ @@ -279,10 +332,16 @@ export const PRISM_MIN_VERTEX_DIST_M = 0.05; /** Minimum top-face width of a Dove prism (width − height, model metres). */ export const DOVE_MIN_TOP_FACE_M = 0.05; /** Distance threshold below which a vertex position is considered degenerate (model metres). */ -export const PRISM_DEGENERATE_DIST = 1e-10; +export const PRISM_DEGENERATE_DIST = LENGTH_DEGENERATE_M; /** Drag-delta magnitude below which a rotation drag move is ignored (model metres). */ export const ROTATION_DRAG_DELTA_MIN = 1e-12; +/** + * Minimum optical-axis offset (m) of a plano/symmetric lens apex from the + * mid-plane. Prevents the apex from crossing flat during curvature drags. + */ +export const LENS_APEX_MIN_OFFSET_M = 0.02; + // ── 10. Light-source rendering ──────────────────────────────────────────────── /** Stroke width for the central glow disc circle (point & arc sources). */ @@ -358,9 +417,18 @@ export const REFLECTION_GRATING_GROOVE_LENGTH_PX = 6; /** Default groove density for new gratings (lines / mm). */ export const GRATING_DEFAULT_LINES_DENSITY = 600; /** Default wavelength (nm) used when a grating ray carries no wavelength. */ -export const GRATING_DEFAULT_WAVELENGTH_NM = 532; +export const GRATING_DEFAULT_WAVELENGTH_NM = GREEN_WAVELENGTH; /** Maximum diffraction order to compute. */ export const GRATING_MAX_DIFFRACTION_ORDER = 10; +/** Default slit-width / line-spacing ratio for new gratings. */ +export const GRATING_DEFAULT_DUTY_CYCLE = 0.5; +/** Groove-density NumberControl bounds (lines / mm). */ +export const GRATING_LINES_DENSITY_MIN = 1; +export const GRATING_LINES_DENSITY_MAX = 2500; +/** Duty-cycle NumberControl bounds and step. */ +export const GRATING_DUTY_CYCLE_MIN = 0.01; +export const GRATING_DUTY_CYCLE_MAX = 0.99; +export const GRATING_DUTY_CYCLE_DELTA = 0.01; // ── 13. Apertured Parabolic Mirror ─────────────────────────────────────────── @@ -393,8 +461,17 @@ export const ACQUISITION_DURATION_S = 2.0; export const ACQUISITION_PASSES_PER_FRAME = 100; /** Initial horizontal offset (px) of the floating chart from the detector midpoint. */ export const DETECTOR_INITIAL_CHART_OFFSET_X = 200; +/** Initial vertical offset (px) of the floating chart from the detector midpoint. */ +export const DETECTOR_INITIAL_CHART_OFFSET_Y = 10; /** Controls the Bézier bulge of the wire connecting detector to chart. */ export const DETECTOR_WIRE_NORMAL_MAGNITUDE = 40; +/** + * Maximum hit samples retained by a detector (reservoir sampling). + * Caps memory while keeping a uniform random sample of irradiance hits. + */ +export const DETECTOR_MAX_HITS = 2000; +/** Headroom factor applied above the max bin value when autoscaling the chart Y axis. */ +export const DETECTOR_CHART_Y_HEADROOM = 1.25; // ── 14. Glass model defaults ───────────────────────────────────────────────── @@ -414,7 +491,7 @@ export const PARALLELOGRAM_PRISM_DEFAULT_HEIGHT_M = 0.42; // ── 15. ContinuousSpectrumSource defaults ──────────────────────────────────── -export const CONT_SPECTRUM_DEFAULT_WL_MIN_NM = 380; +export const CONT_SPECTRUM_DEFAULT_WL_MIN_NM = WAVELENGTH_MIN_NM; export const CONT_SPECTRUM_DEFAULT_WL_STEP_NM = 10; export const CONT_SPECTRUM_DEFAULT_WL_MAX_NM = 700; export const CONT_SPECTRUM_DEFAULT_BRIGHTNESS = 0.5; @@ -437,6 +514,14 @@ export const FIBER_OPTIC_OUTER_RADIUS_MAX_M = 0.25; * coreRadius = outerRadius × this value. */ export const FIBER_CORE_RADIUS_FRACTION_DEFAULT = 0.45; +/** Core-radius-fraction NumberControl bounds and step. */ +export const FIBER_CORE_FRACTION_MIN = 0.05; +export const FIBER_CORE_FRACTION_MAX = 0.95; +export const FIBER_CORE_FRACTION_DELTA = 0.05; +/** Catmull–Rom tension used when sampling the fiber centreline spline. */ +export const FIBER_SPLINE_TENSION = 0.5; +/** Sample intervals per Catmull–Rom segment when building the fiber ribbon. */ +export const FIBER_SAMPLES_PER_SEGMENT = 8; // ── 17. Carousel ───────────────────────────────────────────────────────────── @@ -511,7 +596,12 @@ export const DEFAULT_BEAM_BRIGHTNESS = 0.5; * Default wavelength (nm) for newly created beam, divergent-beam, and * single-ray sources. 532 nm corresponds to a standard green (Nd:YAG) laser. */ -export const DEFAULT_BEAM_WAVELENGTH_NM = 532; +export const DEFAULT_BEAM_WAVELENGTH_NM = GREEN_WAVELENGTH; +/** Beam-splitter transmission NumberControl bounds. */ +export const BEAM_SPLITTER_TRANSMIT_MIN = 0; +export const BEAM_SPLITTER_TRANSMIT_MAX = 1; +/** Step size for beam-splitter transmission NumberControls. */ +export const BEAM_SPLITTER_TRANSMIT_DELTA = 0.05; /** Default brightness for newly created single-ray sources. */ export const DEFAULT_SINGLE_RAY_BRIGHTNESS = 1; /** Default opening half-angle (rad) for newly created arc light sources. */ @@ -564,12 +654,45 @@ export const FACTORY_FIBER_OPTIC_LENGTH_SCALE = 1.5; // ── 22. Element archetype defaults ──────────────────────────────────────────── +/** + * Wavelength (nm) used when a ray segment has no wavelength (canvas/SVG + * rendering fallback) and in the PhET-iO group archetype element state. + */ +export const FALLBACK_RAY_WAVELENGTH_NM = 550; /** * Wavelength (nm) used in the PhET-iO group archetype element state. - * This placeholder value does not represent any particular preset; it is only - * used to satisfy the phetioType schema for the archetype element. + * Alias of {@link FALLBACK_RAY_WAVELENGTH_NM}. + */ +export const ARCHETYPE_DEFAULT_WAVELENGTH_NM = FALLBACK_RAY_WAVELENGTH_NM; + +// ── 22b. Observer defaults ─────────────────────────────────────────────────── + +/** Default observer position X (model metres). */ +export const DEFAULT_OBSERVER_X_M = 0.3; +/** Default observer position Y (model metres). */ +export const DEFAULT_OBSERVER_Y_M = 0; +/** Default observer collection radius (model metres). */ +export const DEFAULT_OBSERVER_RADIUS_M = 0.12; +/** Minimum observer collection radius (model metres). */ +export const OBSERVER_RADIUS_MIN_M = 0.02; +/** Centre-dot radius (px) for the observer node. */ +export const OBSERVER_CENTER_DOT_RADIUS_PX = 7; +/** Rim-handle radius (px) for resizing the observer collection radius. */ +export const OBSERVER_RIM_HANDLE_RADIUS_PX = 5; + +// ── 22c. Track / spatial-index ─────────────────────────────────────────────── + +/** Stroke width for track guide lines. */ +export const TRACK_LINE_WIDTH = 2; +/** Dash pattern [dash, gap] for track guide lines. */ +export const TRACK_LINE_DASH = [8, 4]; +/** Default spatial-hash cell size (model metres). */ +export const SPATIAL_INDEX_CELL_SIZE_M = 2.0; +/** + * Maximum cells traversed along a ray before SpatialIndex falls back to + * testing every element. */ -export const ARCHETYPE_DEFAULT_WAVELENGTH_NM = 550; +export const SPATIAL_INDEX_MAX_TRAVERSAL_STEPS = 200; // ── 23. Fiber optic physics parameters ─────────────────────────────────────── diff --git a/src/common/model/detectors/DetectorElement.ts b/src/common/model/detectors/DetectorElement.ts index d302bae..b24f23f 100644 --- a/src/common/model/detectors/DetectorElement.ts +++ b/src/common/model/detectors/DetectorElement.ts @@ -9,7 +9,7 @@ * Acquisition / histogram logic is delegated to DetectorAcquisition. */ -import { DETECTOR_NUM_BINS } from "../../../OpticsLabConstants.js"; +import { DETECTOR_MAX_HITS, DETECTOR_NUM_BINS } from "../../../OpticsLabConstants.js"; import { ELEMENT_CATEGORY_BLOCKER, ELEMENT_TYPE_DETECTOR } from "../../../OpticsLabStrings.js"; import { BaseSegmentElement } from "../optics/BaseSegmentElement.js"; import { @@ -36,9 +36,6 @@ import type { } from "../optics/OpticsTypes.js"; import { DetectorAcquisition } from "./DetectorAcquisition.js"; -/** Maximum number of hits stored; older hits are replaced via reservoir sampling. */ -export const DETECTOR_MAX_HITS = 2000; - export type DetectorHit = { t: number; brightness: number }; export class DetectorElement extends BaseSegmentElement implements IAcquirable { diff --git a/src/common/model/fiber/FiberOpticElement.ts b/src/common/model/fiber/FiberOpticElement.ts index 0de279c..a680a96 100644 --- a/src/common/model/fiber/FiberOpticElement.ts +++ b/src/common/model/fiber/FiberOpticElement.ts @@ -30,6 +30,8 @@ import { FIBER_OPTIC_CRITICAL_BEND_RADIUS, FIBER_OPTIC_DEFAULT_BEND_LOSS_COEFF, FIBER_OPTIC_DEFAULT_OUTER_RADIUS_M, + FIBER_SAMPLES_PER_SEGMENT, + FIBER_SPLINE_TENSION, } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_FIBER_CORE_GLASS, ELEMENT_TYPE_FIBER_OPTIC } from "../../../OpticsLabStrings.js"; import { Glass, type GlassPathPoint } from "../glass/Glass.js"; @@ -178,8 +180,6 @@ export class FiberCoreGlass extends Glass { // ── Catmull–Rom maths ───────────────────────────────────────────────────────── -const TENSION = 0.5; - /** * Position on the Catmull–Rom segment from p1 to p2, with context points * p0 (before) and p3 (after), evaluated at parameter t ∈ [0, 1]. @@ -187,7 +187,7 @@ const TENSION = 0.5; function crPoint(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { const t2 = t * t; const t3 = t2 * t; - const tension = TENSION; + const tension = FIBER_SPLINE_TENSION; const c0 = tension * (-t3 + 2 * t2 - t); const c1 = (2 - tension) * t3 + (tension - 3) * t2 + 1; const c2 = (tension - 2) * t3 + (3 - 2 * tension) * t2 + tension * t; @@ -203,7 +203,7 @@ function crPoint(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { * The tangent at t=0 is τ*(p2−p0) and at t=1 is τ*(p3−p1). */ function crTangent(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { - const tension = TENSION; + const tension = FIBER_SPLINE_TENSION; const d0 = tension * (-3 * t * t + 4 * t - 1); const d1 = 3 * (2 - tension) * t * t + 2 * (tension - 3) * t; const d2 = 3 * (tension - 2) * t * t + 2 * (3 - 2 * tension) * t + tension; @@ -219,7 +219,7 @@ function crTangent(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point * Used for computing local curvature: κ = |x'y'' − y'x''| / (x'² + y'²)^(3/2). */ function crSecondDerivative(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { - const tension = TENSION; + const tension = FIBER_SPLINE_TENSION; const dd0 = tension * (-6 * t + 4); const dd1 = 6 * (2 - tension) * t + 2 * (tension - 3); const dd2 = 6 * (tension - 2) * t + 2 * (3 - 2 * tension); @@ -230,11 +230,6 @@ function crSecondDerivative(p0: Point, p1: Point, p2: Point, p3: Point, t: numbe }; } -// ── Number of samples used to approximate the spline ───────────────────────── - -/** Intervals per Catmull–Rom segment. Total samples = 4 × N + 1. */ -const N_PER_SEG = 8; - // ── Ribbon path builder ─────────────────────────────────────────────────────── function buildRibbonPath(samples: Array<{ point: Point; tangent: Point }>, r: number): GlassPathPoint[] { @@ -333,7 +328,7 @@ export class FiberOpticElement extends Glass implements ICompound { } /** - * Sample the Catmull–Rom spline at N_PER_SEG × 4 + 1 evenly-spaced + * Sample the Catmull–Rom spline at FIBER_SAMPLES_PER_SEGMENT × 4 + 1 evenly-spaced * parameter values. Returns { point, tangent, curvature } triples in model space. * * Phantom end-points are mirrored so the curve interpolates p1 and p2 @@ -362,9 +357,9 @@ export class FiberOpticElement extends Glass implements ICompound { continue; } const [a, b, c, d] = seg; - const kMax = s < segs.length - 1 ? N_PER_SEG : N_PER_SEG + 1; + const kMax = s < segs.length - 1 ? FIBER_SAMPLES_PER_SEGMENT : FIBER_SAMPLES_PER_SEGMENT + 1; for (let k = 0; k < kMax; k++) { - const t = k / N_PER_SEG; + const t = k / FIBER_SAMPLES_PER_SEGMENT; const tangent = crTangent(a, b, c, d, t); const dd = crSecondDerivative(a, b, c, d, t); const speedSq = tangent.x * tangent.x + tangent.y * tangent.y; diff --git a/src/common/model/glass/Glass.ts b/src/common/model/glass/Glass.ts index e1926fd..d763869 100644 --- a/src/common/model/glass/Glass.ts +++ b/src/common/model/glass/Glass.ts @@ -14,6 +14,7 @@ * from the previous vertex through the control point to the next vertex. */ +import { DEFAULT_CAUCHY_B, DEFAULT_REFRACTIVE_INDEX } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_GLASS } from "../../../OpticsLabStrings.js"; import { arcBounds, @@ -75,7 +76,12 @@ export class Glass extends BaseGlass { public path: GlassPathPoint[]; - public constructor(path: GlassPathPoint[], refIndex = 1.5, cauchyB = 0.004, partialReflect = true) { + public constructor( + path: GlassPathPoint[], + refIndex = DEFAULT_REFRACTIVE_INDEX, + cauchyB = DEFAULT_CAUCHY_B, + partialReflect = true, + ) { super(refIndex, cauchyB, partialReflect); this.path = path; } diff --git a/src/common/model/glass/HalfPlaneGlass.ts b/src/common/model/glass/HalfPlaneGlass.ts index 7cfce66..4d5256a 100644 --- a/src/common/model/glass/HalfPlaneGlass.ts +++ b/src/common/model/glass/HalfPlaneGlass.ts @@ -7,6 +7,7 @@ * looking from p1 toward p2. */ +import { DEFAULT_REFRACTIVE_INDEX } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_PLANE_GLASS } from "../../../OpticsLabStrings.js"; import { type Bounds, type Point, point, rayLineIntersection, segment, segmentNormal } from "../optics/Geometry.js"; import type { IntersectionResult, RayCallConfig, RayInteractionResult, SimulationRay } from "../optics/OpticsTypes.js"; @@ -18,7 +19,7 @@ export class HalfPlaneGlass extends BaseGlass { public p1: Point; public p2: Point; - public constructor(p1: Point, p2: Point, refIndex = 1.5) { + public constructor(p1: Point, p2: Point, refIndex = DEFAULT_REFRACTIVE_INDEX) { // cauchyB=0 preserves current behaviour (no dispersion); partialReflect=true super(refIndex, 0, true); this.p1 = p1; diff --git a/src/common/model/glass/SlabGlass.ts b/src/common/model/glass/SlabGlass.ts index 6c2231f..1f0a7ef 100644 --- a/src/common/model/glass/SlabGlass.ts +++ b/src/common/model/glass/SlabGlass.ts @@ -1,4 +1,9 @@ -import { SLAB_GLASS_DEFAULT_HEIGHT_M, SLAB_GLASS_DEFAULT_WIDTH_M } from "../../../OpticsLabConstants.js"; +import { + DEFAULT_CAUCHY_B, + DEFAULT_REFRACTIVE_INDEX, + SLAB_GLASS_DEFAULT_HEIGHT_M, + SLAB_GLASS_DEFAULT_WIDTH_M, +} from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_SLAB_GLASS } from "../../../OpticsLabStrings.js"; import type { Point } from "../optics/Geometry.js"; import { DimensionalGlass } from "./DimensionalGlass.js"; @@ -22,8 +27,8 @@ export class SlabGlass extends DimensionalGlass { center: Point, width = SLAB_GLASS_DEFAULT_WIDTH_M, height = SLAB_GLASS_DEFAULT_HEIGHT_M, - refIndex = 1.5, - cauchyB = 0.004, + refIndex = DEFAULT_REFRACTIVE_INDEX, + cauchyB = DEFAULT_CAUCHY_B, partialReflect = true, ) { super(makeVertices(center.x, center.y, width, height), width, height, refIndex, cauchyB, partialReflect); diff --git a/src/common/model/gratings/ReflectionGrating.ts b/src/common/model/gratings/ReflectionGrating.ts index 1985957..1c08594 100644 --- a/src/common/model/gratings/ReflectionGrating.ts +++ b/src/common/model/gratings/ReflectionGrating.ts @@ -11,7 +11,7 @@ * - dutyCycle: slit-width to line-spacing ratio (0–1), controls order intensities */ -import { GRATING_DEFAULT_LINES_DENSITY } from "../../../OpticsLabConstants.js"; +import { GRATING_DEFAULT_DUTY_CYCLE, GRATING_DEFAULT_LINES_DENSITY } from "../../../OpticsLabConstants.js"; import { ELEMENT_CATEGORY_MIRROR, ELEMENT_TYPE_REFLECTION_GRATING } from "../../../OpticsLabStrings.js"; import { BaseSegmentElement } from "../optics/BaseSegmentElement.js"; import type { Point } from "../optics/Geometry.js"; @@ -32,7 +32,12 @@ export class ReflectionGrating extends BaseSegmentElement { /** Slit-width / line-spacing ratio (0–1). */ public dutyCycle: number; - public constructor(p1: Point, p2: Point, linesDensity = GRATING_DEFAULT_LINES_DENSITY, dutyCycle = 0.5) { + public constructor( + p1: Point, + p2: Point, + linesDensity = GRATING_DEFAULT_LINES_DENSITY, + dutyCycle = GRATING_DEFAULT_DUTY_CYCLE, + ) { super(p1, p2); this.linesDensity = linesDensity; this.dutyCycle = dutyCycle; diff --git a/src/common/model/gratings/TransmissionGrating.ts b/src/common/model/gratings/TransmissionGrating.ts index 37140f5..6cc1959 100644 --- a/src/common/model/gratings/TransmissionGrating.ts +++ b/src/common/model/gratings/TransmissionGrating.ts @@ -11,7 +11,7 @@ * - dutyCycle: slit-width to line-spacing ratio (0–1), controls order intensities */ -import { GRATING_DEFAULT_LINES_DENSITY } from "../../../OpticsLabConstants.js"; +import { GRATING_DEFAULT_DUTY_CYCLE, GRATING_DEFAULT_LINES_DENSITY } from "../../../OpticsLabConstants.js"; import { ELEMENT_CATEGORY_GLASS, ELEMENT_TYPE_TRANSMISSION_GRATING } from "../../../OpticsLabStrings.js"; import { BaseSegmentElement } from "../optics/BaseSegmentElement.js"; import type { Point } from "../optics/Geometry.js"; @@ -32,7 +32,12 @@ export class TransmissionGrating extends BaseSegmentElement { /** Slit-width / line-spacing ratio (0–1). */ public dutyCycle: number; - public constructor(p1: Point, p2: Point, linesDensity = GRATING_DEFAULT_LINES_DENSITY, dutyCycle = 0.5) { + public constructor( + p1: Point, + p2: Point, + linesDensity = GRATING_DEFAULT_LINES_DENSITY, + dutyCycle = GRATING_DEFAULT_DUTY_CYCLE, + ) { super(p1, p2); this.linesDensity = linesDensity; this.dutyCycle = dutyCycle; diff --git a/src/common/model/light-sources/ArcLightSource.ts b/src/common/model/light-sources/ArcLightSource.ts index bb0aa97..73fc636 100644 --- a/src/common/model/light-sources/ArcLightSource.ts +++ b/src/common/model/light-sources/ArcLightSource.ts @@ -8,7 +8,7 @@ * π/2 = down (positive y-axis), consistent with Math.atan2(dy, dx). */ -import { DEFAULT_ARC_CONE_HALF_ANGLE_RAD } from "../../../OpticsLabConstants.js"; +import { DEFAULT_ARC_BRIGHTNESS, DEFAULT_ARC_CONE_HALF_ANGLE_RAD } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_ARC_SOURCE } from "../../../OpticsLabStrings.js"; import type { Bounds, Point } from "../optics/Geometry.js"; import { normalize, point } from "../optics/Geometry.js"; @@ -38,7 +38,7 @@ export class ArcLightSource extends BaseLightSource { position: Point, direction = 0, emissionAngle = DEFAULT_ARC_CONE_HALF_ANGLE_RAD, - brightness = 0.5, + brightness = DEFAULT_ARC_BRIGHTNESS, wavelength = GREEN_WAVELENGTH, ) { super(brightness, wavelength); diff --git a/src/common/model/light-sources/BeamSource.ts b/src/common/model/light-sources/BeamSource.ts index 30e6a28..325ec06 100644 --- a/src/common/model/light-sources/BeamSource.ts +++ b/src/common/model/light-sources/BeamSource.ts @@ -6,7 +6,9 @@ * the segment. */ +import { DEFAULT_BEAM_BRIGHTNESS } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_BEAM } from "../../../OpticsLabStrings.js"; + import type { Bounds, Point } from "../optics/Geometry.js"; import { distance, normalize, point, pointsBounds, subtract } from "../optics/Geometry.js"; import { BEAM_RAY_DENSITY_SCALE, BRIGHTNESS_CONTINUOUS_THRESHOLD } from "../optics/OpticsConstants.js"; @@ -22,7 +24,7 @@ export class BeamSource extends BaseLightSource { /** Second endpoint of the segment perpendicular to beam direction. */ public p2: Point; - public constructor(p1: Point, p2: Point, brightness = 0.5, wavelength = GREEN_WAVELENGTH) { + public constructor(p1: Point, p2: Point, brightness = DEFAULT_BEAM_BRIGHTNESS, wavelength = GREEN_WAVELENGTH) { super(brightness, wavelength); this.p1 = p1; this.p2 = p2; diff --git a/src/common/model/light-sources/DivergentBeam.ts b/src/common/model/light-sources/DivergentBeam.ts index 72f13c1..79c1ae6 100644 --- a/src/common/model/light-sources/DivergentBeam.ts +++ b/src/common/model/light-sources/DivergentBeam.ts @@ -6,6 +6,7 @@ * segment within a finite divergence half-angle. */ +import { DEFAULT_BEAM_BRIGHTNESS, DIVERGENT_BEAM_DEFAULT_EMIS_ANGLE_DEG } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_DIVERGENT_BEAM } from "../../../OpticsLabStrings.js"; import type { Bounds, Point } from "../optics/Geometry.js"; import { distance, normalize, point, pointsBounds, subtract } from "../optics/Geometry.js"; @@ -18,9 +19,6 @@ import type { SimulationRay, ViewMode } from "../optics/OpticsTypes.js"; import { BaseLightSource } from "./BaseLightSource.js"; import { GREEN_WAVELENGTH } from "./LightSourceConstants.js"; -/** Default divergence half-angle in degrees. */ -export const DIVERGENT_BEAM_DEFAULT_EMIS_ANGLE = 10; - export class DivergentBeam extends BaseLightSource { public readonly type = ELEMENT_TYPE_DIVERGENT_BEAM; @@ -34,9 +32,9 @@ export class DivergentBeam extends BaseLightSource { public constructor( p1: Point, p2: Point, - brightness = 0.5, + brightness = DEFAULT_BEAM_BRIGHTNESS, wavelength = GREEN_WAVELENGTH, - emisAngle = DIVERGENT_BEAM_DEFAULT_EMIS_ANGLE, + emisAngle = DIVERGENT_BEAM_DEFAULT_EMIS_ANGLE_DEG, ) { super(brightness, wavelength); this.p1 = p1; diff --git a/src/common/model/light-sources/PointSourceElement.ts b/src/common/model/light-sources/PointSourceElement.ts index 2e2e390..7bdd64a 100644 --- a/src/common/model/light-sources/PointSourceElement.ts +++ b/src/common/model/light-sources/PointSourceElement.ts @@ -5,7 +5,9 @@ * directions. The angular spacing of rays depends on rayDensity. */ +import { DEFAULT_POINT_SOURCE_BRIGHTNESS } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_POINT_SOURCE } from "../../../OpticsLabStrings.js"; + import type { Bounds, Point } from "../optics/Geometry.js"; import { point } from "../optics/Geometry.js"; import { BRIGHTNESS_CONTINUOUS_THRESHOLD, RAY_DENSITY_SCALE } from "../optics/OpticsConstants.js"; @@ -18,7 +20,7 @@ export class PointSourceElement extends BaseLightSource { public position: Point; - public constructor(position: Point, brightness = 0.5, wavelength = GREEN_WAVELENGTH) { + public constructor(position: Point, brightness = DEFAULT_POINT_SOURCE_BRIGHTNESS, wavelength = GREEN_WAVELENGTH) { super(brightness, wavelength); this.position = position; } diff --git a/src/common/model/light-sources/SingleRaySource.ts b/src/common/model/light-sources/SingleRaySource.ts index 37792df..f474648 100644 --- a/src/common/model/light-sources/SingleRaySource.ts +++ b/src/common/model/light-sources/SingleRaySource.ts @@ -5,7 +5,9 @@ * a second point that indicates direction. Emits exactly one ray. */ +import { DEFAULT_SINGLE_RAY_BRIGHTNESS } from "../../../OpticsLabConstants.js"; import { ELEMENT_TYPE_SINGLE_RAY } from "../../../OpticsLabStrings.js"; + import type { Bounds, Point } from "../optics/Geometry.js"; import { normalize, point, pointsBounds, subtract } from "../optics/Geometry.js"; import type { SimulationRay, ViewMode } from "../optics/OpticsTypes.js"; @@ -18,7 +20,7 @@ export class SingleRaySource extends BaseLightSource { public p1: Point; public p2: Point; - public constructor(p1: Point, p2: Point, brightness = 1, wavelength = GREEN_WAVELENGTH) { + public constructor(p1: Point, p2: Point, brightness = DEFAULT_SINGLE_RAY_BRIGHTNESS, wavelength = GREEN_WAVELENGTH) { super(brightness, wavelength); this.p1 = p1; this.p2 = p2; diff --git a/src/common/model/mirrors/BeamSplitterElement.ts b/src/common/model/mirrors/BeamSplitterElement.ts index cfcaf6a..a35d7d4 100644 --- a/src/common/model/mirrors/BeamSplitterElement.ts +++ b/src/common/model/mirrors/BeamSplitterElement.ts @@ -7,7 +7,9 @@ * in the original direction. */ +import { DEFAULT_BEAM_SPLITTER_TRANSMIT } from "../../../OpticsLabConstants.js"; import { ELEMENT_CATEGORY_MIRROR, ELEMENT_TYPE_BEAM_SPLITTER } from "../../../OpticsLabStrings.js"; + import { BaseSegmentElement } from "../optics/BaseSegmentElement.js"; import { dot, normalize, type Point, point } from "../optics/Geometry.js"; import type { @@ -24,7 +26,7 @@ export class BeamSplitterElement extends BaseSegmentElement { /** Fraction of brightness transmitted (0..1). The rest is reflected. */ public transRatio: number; - public constructor(p1: Point, p2: Point, transRatio = 0.5) { + public constructor(p1: Point, p2: Point, transRatio = DEFAULT_BEAM_SPLITTER_TRANSMIT) { super(p1, p2); this.transRatio = transRatio; } diff --git a/src/common/model/optics/CommandHistory.ts b/src/common/model/optics/CommandHistory.ts index 7ff3f03..3e71c18 100644 --- a/src/common/model/optics/CommandHistory.ts +++ b/src/common/model/optics/CommandHistory.ts @@ -1,3 +1,5 @@ +import { MAX_HISTORY_SIZE } from "../../../OpticsLabConstants.js"; + /** * CommandHistory.ts * @@ -15,8 +17,6 @@ * The history is capped at MAX_HISTORY_SIZE to bound memory use. */ -export const MAX_HISTORY_SIZE = 100; - /** A discrete, reversible operation. */ export interface SceneCommand { /** Apply (or re-apply) this command. */ diff --git a/src/common/model/optics/OpticsConstants.ts b/src/common/model/optics/OpticsConstants.ts index cf70371..e3eeef2 100644 --- a/src/common/model/optics/OpticsConstants.ts +++ b/src/common/model/optics/OpticsConstants.ts @@ -6,6 +6,8 @@ * that file instead. */ +import { BRIGHTNESS_MAX, DEFAULT_RAY_DENSITY, PIXELS_PER_METER } from "../../../OpticsLabConstants.js"; + // ── Intersection tolerance ──────────────────────────────────────────────────── /** @@ -44,7 +46,7 @@ export const RAY_DENSITY_SCALE = 500; * rayDensity was calibrated for pixel coordinates, so this factor (equal to * pixels-per-metre) restores the intended ray count. */ -export const BEAM_RAY_DENSITY_SCALE = 100; +export const BEAM_RAY_DENSITY_SCALE = PIXELS_PER_METER; /** * Brightness value at which a source transitions from discrete-ray mode @@ -59,13 +61,13 @@ export const BRIGHTNESS_CONTINUOUS_THRESHOLD = 1.0; * Normalization divisor that maps the brightness property to a [0, 1] per-ray * intensity value in continuous-beam mode. Equals the maximum allowed brightness. */ -export const BRIGHTNESS_NORMALIZE = 2.0; +export const BRIGHTNESS_NORMALIZE = BRIGHTNESS_MAX; /** * Reference ray density used to calibrate per-ray brightness. * At this density each ray has its "full" brightness. * Above this density, per-ray brightness scales inversely with density so that * total luminosity is conserved: 2× rays → each at ½ brightness. - * Should equal DEFAULT_RAY_DENSITY in OpticsLabConstants. + * Equals DEFAULT_RAY_DENSITY in OpticsLabConstants. */ -export const RAY_DENSITY_REFERENCE = 0.5; +export const RAY_DENSITY_REFERENCE = DEFAULT_RAY_DENSITY; diff --git a/src/common/model/optics/RayTracer.ts b/src/common/model/optics/RayTracer.ts index 14a5a40..e1b8d58 100644 --- a/src/common/model/optics/RayTracer.ts +++ b/src/common/model/optics/RayTracer.ts @@ -20,6 +20,7 @@ import { DEFAULT_RAY_DENSITY, FAR_DISTANCE, MAX_TOTAL_SEGMENTS, + PIXELS_PER_METER, RAY_CONVERGENCE_THRESHOLD, } from "../../../OpticsLabConstants.js"; import { ELEMENT_CATEGORY_LIGHT_SOURCE, VIEW_MODE_RAYS } from "../../../OpticsLabStrings.js"; @@ -407,7 +408,7 @@ export class RayTracer { */ private processObserverImages(allSegments: TracedSegment[], allImages: DetectedImage[]): void { const bySource = this.groupObservedSegmentsBySource(allSegments); - const thresholdSq = (RAY_CONVERGENCE_THRESHOLD / 100) ** 2; + const thresholdSq = (RAY_CONVERGENCE_THRESHOLD / PIXELS_PER_METER) ** 2; for (const segs of bySource.values()) { segs.sort((a, b) => (a.rayIndex ?? 0) - (b.rayIndex ?? 0)); @@ -555,7 +556,7 @@ export class RayTracer { private findImagesInSequence(segs: TracedSegment[], images: DetectedImage[]): void { // Convergence threshold in model metres (RAY_CONVERGENCE_THRESHOLD is in pixels, // 100 px = 1 m). - const thresholdSq = (RAY_CONVERGENCE_THRESHOLD / 100) ** 2; + const thresholdSq = (RAY_CONVERGENCE_THRESHOLD / PIXELS_PER_METER) ** 2; const candidates: DetectedImage[] = []; @@ -595,7 +596,7 @@ export class RayTracer { } // Deduplicate candidates that landed in the same threshold cell. - const thresholdM = RAY_CONVERGENCE_THRESHOLD / 100; + const thresholdM = RAY_CONVERGENCE_THRESHOLD / PIXELS_PER_METER; const seen = new Set(); for (const c of candidates) { const key = `${Math.round(c.position.x / thresholdM)},${Math.round(c.position.y / thresholdM)}`; diff --git a/src/common/model/optics/SpatialIndex.ts b/src/common/model/optics/SpatialIndex.ts index 3920daf..6fc789a 100644 --- a/src/common/model/optics/SpatialIndex.ts +++ b/src/common/model/optics/SpatialIndex.ts @@ -11,19 +11,10 @@ * separate "unbounded" list and always included in every query. */ +import { SPATIAL_INDEX_CELL_SIZE_M, SPATIAL_INDEX_MAX_TRAVERSAL_STEPS } from "../../../OpticsLabConstants.js"; import type { Point } from "./Geometry.js"; import type { OpticalElement } from "./OpticsTypes.js"; -/** Default cell size in model units (metres). */ -const DEFAULT_CELL_SIZE = 2.0; - -/** - * Maximum number of cells to traverse along a ray before falling back to - * the full element list. Prevents degenerate rays from traversing the - * entire grid. - */ -const MAX_TRAVERSAL_STEPS = 200; - export class SpatialIndex { private readonly cellSize: number; private readonly invCellSize: number; @@ -38,7 +29,7 @@ export class SpatialIndex { private gridMaxCX = 0; private gridMaxCY = 0; - public constructor(elements: OpticalElement[], cellSize = DEFAULT_CELL_SIZE) { + public constructor(elements: OpticalElement[], cellSize = SPATIAL_INDEX_CELL_SIZE_M) { this.cellSize = cellSize; this.invCellSize = 1 / cellSize; this.build(elements); @@ -191,7 +182,7 @@ export class SpatialIndex { let tMaxX = xAxis.tMax; let tMaxY = yAxis.tMax; - for (let step = 0; step < MAX_TRAVERSAL_STEPS; step++) { + for (let step = 0; step < SPATIAL_INDEX_MAX_TRAVERSAL_STEPS; step++) { if (this.isInGrid(cx, cy)) { this.collectCell(cx, cy, seen, result); } else if (this.isOutOfGrid(cx, cy, xAxis.step, yAxis.step)) { diff --git a/src/common/view/BaseOpticalElementView.ts b/src/common/view/BaseOpticalElementView.ts index 9753a10..1f72925 100644 --- a/src/common/view/BaseOpticalElementView.ts +++ b/src/common/view/BaseOpticalElementView.ts @@ -22,11 +22,11 @@ import { Emitter, type TReadOnlyProperty } from "scenerystack/axon"; import { Bounds2 } from "scenerystack/dot"; import { Node, Rectangle, type RichDragListener } from "scenerystack/scenery"; import OpticsLabColors from "../../OpticsLabColors.js"; +import { SELECTION_PAD_PX } from "../../OpticsLabConstants.js"; + import OpticsLabNamespace from "../../OpticsLabNamespace.js"; import { unlinkHandleVisibility } from "./ViewHelpers.js"; -const SELECTION_PAD = 8; // px of padding around content bounds - export abstract class BaseOpticalElementView extends Node { /** Drag listener used to translate the element as a whole. */ public abstract readonly bodyDragListener: RichDragListener; @@ -102,10 +102,10 @@ export abstract class BaseOpticalElementView extends Node { return; } this._selectionFrame.setRect( - b.minX - SELECTION_PAD, - b.minY - SELECTION_PAD, - b.width + 2 * SELECTION_PAD, - b.height + 2 * SELECTION_PAD, + b.minX - SELECTION_PAD_PX, + b.minY - SELECTION_PAD_PX, + b.width + 2 * SELECTION_PAD_PX, + b.height + 2 * SELECTION_PAD_PX, ); } diff --git a/src/common/view/GridScaleIndicatorNode.ts b/src/common/view/GridScaleIndicatorNode.ts index faf4711..b7422b4 100644 --- a/src/common/view/GridScaleIndicatorNode.ts +++ b/src/common/view/GridScaleIndicatorNode.ts @@ -15,6 +15,8 @@ import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Node, Path, Rectangle, Text } from "scenerystack/scenery"; import { PhetFont } from "scenerystack/scenery-phet"; import OpticsLabColors from "../../OpticsLabColors.js"; +import { HANDLE_LINE_WIDTH } from "../../OpticsLabConstants.js"; + import OpticsLabNamespace from "../../OpticsLabNamespace.js"; // ── Visual constants (view pixels) ──────────────────────────────────────────── @@ -45,7 +47,7 @@ export class GridScaleIndicatorNode extends Node { // overlayValueFillProperty is #eee on dark backgrounds, #111 on light ones. this._arrowPath = new Path(null, { stroke: OpticsLabColors.overlayValueFillProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineCap: "round", lineJoin: "round", }); diff --git a/src/common/view/ImageOverlayNode.ts b/src/common/view/ImageOverlayNode.ts index c6d7cb1..f9c9c4e 100644 --- a/src/common/view/ImageOverlayNode.ts +++ b/src/common/view/ImageOverlayNode.ts @@ -17,12 +17,15 @@ import { toFixed } from "scenerystack/dot"; import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Circle, Node, Text } from "scenerystack/scenery"; import OpticsLabColors from "../../OpticsLabColors.js"; -import { FONT_BOLD_9PX } from "../../OpticsLabConstants.js"; +import { + FONT_BOLD_9PX, + HANDLE_LINE_WIDTH, + IMAGE_LABEL_OFFSET_X_PX, + IMAGE_MARKER_RADIUS_PX, +} from "../../OpticsLabConstants.js"; import OpticsLabNamespace from "../../OpticsLabNamespace.js"; import type { DetectedImage } from "../model/optics/OpticsTypes.js"; -const MARKER_RADIUS = 6; // px -const LABEL_OFFSET_X = MARKER_RADIUS + 3; const LABEL_FONT = FONT_BOLD_9PX; export class ImageOverlayNode extends Node { @@ -84,10 +87,10 @@ export class ImageOverlayNode extends Node { if (img.imageType === "real") { const fillBase = OpticsLabColors.imageRealFillBaseColorProperty.value; const strokeBase = OpticsLabColors.imageRealStrokeBaseColorProperty.value; - marker = new Circle(MARKER_RADIUS, { + marker = new Circle(IMAGE_MARKER_RADIUS_PX, { fill: `rgba(${fillBase.r},${fillBase.g},${fillBase.b},${toFixed(alpha * 0.85, 3)})`, stroke: `rgba(${strokeBase.r},${strokeBase.g},${strokeBase.b},${toFixed(alpha, 3)})`, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, x: vx, y: vy, }); @@ -95,10 +98,10 @@ export class ImageOverlayNode extends Node { labelFill = OpticsLabColors.imageRealLabelFillProperty.value.toCSS(); } else if (img.imageType === "virtualObject") { const strokeBase = OpticsLabColors.imageVirtualObjectStrokeBaseColorProperty.value; - marker = new Circle(MARKER_RADIUS, { + marker = new Circle(IMAGE_MARKER_RADIUS_PX, { fill: null, stroke: `rgba(${strokeBase.r},${strokeBase.g},${strokeBase.b},${toFixed(alpha, 3)})`, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineDash: [3, 2], x: vx, y: vy, @@ -107,10 +110,10 @@ export class ImageOverlayNode extends Node { labelFill = OpticsLabColors.imageVirtualObjectLabelFillProperty.value.toCSS(); } else { const strokeBase = OpticsLabColors.imageVirtualStrokeBaseColorProperty.value; - marker = new Circle(MARKER_RADIUS, { + marker = new Circle(IMAGE_MARKER_RADIUS_PX, { fill: null, stroke: `rgba(${strokeBase.r},${strokeBase.g},${strokeBase.b},${toFixed(alpha, 3)})`, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineDash: [3, 2], x: vx, y: vy, @@ -122,7 +125,7 @@ export class ImageOverlayNode extends Node { const label = new Text(labelText, { font: LABEL_FONT, fill: labelFill, - x: vx + LABEL_OFFSET_X, + x: vx + IMAGE_LABEL_OFFSET_X_PX, y: vy + 3, }); diff --git a/src/common/view/ObserverNode.ts b/src/common/view/ObserverNode.ts index a694813..49c89ce 100644 --- a/src/common/view/ObserverNode.ts +++ b/src/common/view/ObserverNode.ts @@ -18,18 +18,24 @@ import { Circle, Node, RichDragListener, Text } from "scenerystack/scenery"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import OpticsLabColors from "../../OpticsLabColors.js"; -import { FONT_11PX } from "../../OpticsLabConstants.js"; +import { + DEFAULT_OBSERVER_RADIUS_M, + DEFAULT_OBSERVER_X_M, + DEFAULT_OBSERVER_Y_M, + FONT_11PX, + HANDLE_LINE_WIDTH, + OBSERVER_CENTER_DOT_RADIUS_PX, + OBSERVER_RADIUS_MIN_M, + OBSERVER_RIM_HANDLE_RADIUS_PX, +} from "../../OpticsLabConstants.js"; import OpticsLabNamespace from "../../OpticsLabNamespace.js"; import type { Observer } from "../model/optics/OpticsTypes.js"; export const DEFAULT_OBSERVER: Observer = { - position: { x: 0.3, y: 0 }, - radius: 0.12, + position: { x: DEFAULT_OBSERVER_X_M, y: DEFAULT_OBSERVER_Y_M }, + radius: DEFAULT_OBSERVER_RADIUS_M, }; -const CENTER_DOT_RADIUS = 7; // px -const RIM_HANDLE_RADIUS = 5; // px - export class ObserverNode extends Node { private readonly modelViewTransform: ModelViewTransform2; private readonly observerProperty: Property; @@ -50,16 +56,16 @@ export class ObserverNode extends Node { this.radiusCircle = new Circle(0, { stroke: OpticsLabColors.observerCircleStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineDash: [5, 3], fill: OpticsLabColors.observerCircleFillProperty, pickable: false, }); - this.centerDot = new Circle(CENTER_DOT_RADIUS, { + this.centerDot = new Circle(OBSERVER_CENTER_DOT_RADIUS_PX, { fill: OpticsLabColors.observerDotFillProperty, stroke: OpticsLabColors.observerDotStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, cursor: "move", }); this.centerDot.tagName = "div"; @@ -71,7 +77,7 @@ export class ObserverNode extends Node { fill: OpticsLabColors.observerLabelFillProperty, }); - this.rimHandle = new Circle(RIM_HANDLE_RADIUS, { + this.rimHandle = new Circle(OBSERVER_RIM_HANDLE_RADIUS_PX, { fill: OpticsLabColors.observerRimFillProperty, stroke: OpticsLabColors.observerRimStrokeProperty, lineWidth: 1, @@ -127,7 +133,7 @@ export class ObserverNode extends Node { const current = observerProperty.value ?? DEFAULT_OBSERVER; observerProperty.value = { position: current.position, - radius: Math.max(0.02, startRadius + accR), + radius: Math.max(OBSERVER_RADIUS_MIN_M, startRadius + accR), }; }, }); @@ -156,7 +162,7 @@ export class ObserverNode extends Node { this.centerDot.y = cy; this.labelNode.centerX = cx; - this.labelNode.bottom = cy - (CENTER_DOT_RADIUS + 3); + this.labelNode.bottom = cy - (OBSERVER_CENTER_DOT_RADIUS_PX + 3); this.radiusCircle.x = cx; this.radiusCircle.y = cy; diff --git a/src/common/view/RayPropagationView.ts b/src/common/view/RayPropagationView.ts index 5b5db6e..c05ac9e 100644 --- a/src/common/view/RayPropagationView.ts +++ b/src/common/view/RayPropagationView.ts @@ -27,6 +27,7 @@ import { EXT_LINE_DASH, EXT_LINE_WIDTH, EXT_R, + FALLBACK_RAY_WAVELENGTH_NM, RAY_ALPHA_BUCKETS, RAY_ALPHA_SCALE, RAY_ALPHA_SKIP, @@ -302,7 +303,7 @@ export class RayPropagationView extends CanvasNode { return; } - const c = VisibleColor.wavelengthToColor(seg.wavelength ?? 550); + const c = VisibleColor.wavelengthToColor(seg.wavelength ?? FALLBACK_RAY_WAVELENGTH_NM); let obsAlpha = seg.isObserved ? Math.min(1, alpha * 1.4) : alpha; if (additive) { obsAlpha = Math.min(1, obsAlpha * CONT_SPECTRUM_RAY_ALPHA_MULTIPLIER); @@ -365,7 +366,7 @@ export class RayPropagationView extends CanvasNode { continue; } - const c = VisibleColor.wavelengthToColor(seg.wavelength ?? 550); + const c = VisibleColor.wavelengthToColor(seg.wavelength ?? FALLBACK_RAY_WAVELENGTH_NM); context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${toFixed(alpha, 3)})`; context.beginPath(); context.moveTo(clipped[0], clipped[1]); @@ -440,7 +441,7 @@ export class RayPropagationView extends CanvasNode { const baseX = mx - ux * (ARROW_LENGTH * 0.5); const baseY = my - uy * (ARROW_LENGTH * 0.5); - const c = VisibleColor.wavelengthToColor(seg.wavelength ?? 550); + const c = VisibleColor.wavelengthToColor(seg.wavelength ?? FALLBACK_RAY_WAVELENGTH_NM); context.fillStyle = `rgba(${c.r},${c.g},${c.b},${toFixed(alpha, 3)})`; context.beginPath(); context.moveTo(tipX, tipY); diff --git a/src/common/view/RayStubsProperty.ts b/src/common/view/RayStubsProperty.ts deleted file mode 100644 index ef0fa80..0000000 --- a/src/common/view/RayStubsProperty.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * RayStubsProperty.ts - * - * Singleton properties that control the "ray stubs" display mode. - * When enabled, only a short stub of each ray segment is rendered from its - * start point, hiding the full ray path. This gives a clean directional - * indicator without the visual clutter of complete ray traces. - * - * rayStubsEnabledProperty – toggles the mode on / off - * rayStubLengthPxProperty – length of each stub in view pixels (default 50) - */ - -import { BooleanProperty, NumberProperty } from "scenerystack/axon"; -import { Range } from "scenerystack/dot"; -import OpticsLabNamespace from "../../OpticsLabNamespace.js"; - -export const RAY_STUB_LENGTH_DEFAULT_PX = 50; -export const RAY_STUB_LENGTH_MIN_PX = 10; -export const RAY_STUB_LENGTH_MAX_PX = 200; - -export const rayStubsEnabledProperty = new BooleanProperty(false); - -export const rayStubLengthPxProperty = new NumberProperty(RAY_STUB_LENGTH_DEFAULT_PX, { - range: new Range(RAY_STUB_LENGTH_MIN_PX, RAY_STUB_LENGTH_MAX_PX), -}); - -OpticsLabNamespace.register("rayStubsEnabledProperty", rayStubsEnabledProperty); -OpticsLabNamespace.register("rayStubLengthPxProperty", rayStubLengthPxProperty); diff --git a/src/common/view/SceneSVGExporter.ts b/src/common/view/SceneSVGExporter.ts index 851aceb..35cb5b0 100644 --- a/src/common/view/SceneSVGExporter.ts +++ b/src/common/view/SceneSVGExporter.ts @@ -15,6 +15,7 @@ import { EXT_LINE_DASH, EXT_LINE_WIDTH, EXT_R, + FALLBACK_RAY_WAVELENGTH_NM, PIXELS_PER_METER, PROTRACTOR_SCALE, RAY_ALPHA_BUCKETS, @@ -158,7 +159,7 @@ function addRayPathNodes( const shape = getOrCreateExtensionShape(extensionShapes, alphaBucket); shape.moveTo(x1, y1).lineTo(x2, y2); } else { - const wavelength = seg.wavelength ?? 550; + const wavelength = seg.wavelength ?? FALLBACK_RAY_WAVELENGTH_NM; const wavelengthColor = getCachedWavelengthColor(wavelengthColorCache, wavelength); const bucket = getOrCreateForwardBucket(forwardBuckets, wavelengthColor, alphaBucket); bucket.shape.moveTo(x1, y1).lineTo(x2, y2); diff --git a/src/common/view/ToolsPanelIcons.ts b/src/common/view/ToolsPanelIcons.ts index 24e9be2..df744df 100644 --- a/src/common/view/ToolsPanelIcons.ts +++ b/src/common/view/ToolsPanelIcons.ts @@ -82,7 +82,7 @@ export function extendedRaysIcon(): Node { const spreadY = 4; return new Node({ children: [ - new Line(startX, cy, endX, cy, { stroke: c, lineWidth: 1.5, lineDash: dash }), + new Line(startX, cy, endX, cy, { stroke: c, lineWidth: HANDLE_LINE_WIDTH, lineDash: dash }), new Line(midX, cy, endX, cy - spreadY, { stroke: c, lineWidth: 1, lineDash: dash }), new Line(midX, cy, endX, cy + spreadY, { stroke: c, lineWidth: 1, lineDash: dash }), ], @@ -130,7 +130,10 @@ export function rayArrowsIcon(): Node { .lineTo(tipX - headH, cy + headW) .close(); return new Node({ - children: [new Line(left, cy, right, cy, { stroke: c, lineWidth: 1.5 }), new Path(headShape, { fill: c })], + children: [ + new Line(left, cy, right, cy, { stroke: c, lineWidth: HANDLE_LINE_WIDTH }), + new Path(headShape, { fill: c }), + ], }); } @@ -143,8 +146,8 @@ export function rayStubsIcon(): Node { const gap = 2; return new Node({ children: [ - new Line(cx - gap / 2 - stubLen, cy, cx - gap / 2, cy, { stroke: c, lineWidth: 1.5 }), - new Line(cx + gap / 2, cy, cx + gap / 2 + stubLen, cy, { stroke: c, lineWidth: 1.5 }), + new Line(cx - gap / 2 - stubLen, cy, cx - gap / 2, cy, { stroke: c, lineWidth: HANDLE_LINE_WIDTH }), + new Line(cx + gap / 2, cy, cx + gap / 2 + stubLen, cy, { stroke: c, lineWidth: HANDLE_LINE_WIDTH }), ], }); } diff --git a/src/common/view/detectors/DetectorChartPanel.ts b/src/common/view/detectors/DetectorChartPanel.ts index 52cf4d1..7101582 100644 --- a/src/common/view/detectors/DetectorChartPanel.ts +++ b/src/common/view/detectors/DetectorChartPanel.ts @@ -28,9 +28,11 @@ import OpticsLabColors from "../../../OpticsLabColors.js"; import { DETECTOR_CHART_HEIGHT, DETECTOR_CHART_WIDTH, + DETECTOR_CHART_Y_HEADROOM, DETECTOR_NUM_BINS, FONT_9PX, FONT_BOLD_9PX, + HANDLE_LINE_WIDTH, PANEL_CORNER_RADIUS, PANEL_X_MARGIN, PANEL_Y_MARGIN, @@ -123,7 +125,7 @@ export class DetectorChartPanel extends Panel { // Reference tick marks crossing the bottom border of the chart const chartTicksPath = new Path(buildChartTicksShape(chartTransform), { stroke: OpticsLabColors.detectorTickStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineCap: "round", pickable: false, }); @@ -267,7 +269,7 @@ export class DetectorChartPanel extends Panel { } } } - this.chartTransform.modelYRange = new Range(0, maxVal * 1.25); + this.chartTransform.modelYRange = new Range(0, maxVal * DETECTOR_CHART_Y_HEADROOM); // Update live bars const liveData = liveBins.map((v, i) => new Vector2((i + 0.5) / numBins, v)); diff --git a/src/common/view/detectors/DetectorView.ts b/src/common/view/detectors/DetectorView.ts index 3c65b54..7e6241d 100644 --- a/src/common/view/detectors/DetectorView.ts +++ b/src/common/view/detectors/DetectorView.ts @@ -29,7 +29,9 @@ import OpticsLabColors from "../../../OpticsLabColors.js"; import { ARC_MIRROR_SAMPLE_COUNT, DETECTOR_INITIAL_CHART_OFFSET_X, + DETECTOR_INITIAL_CHART_OFFSET_Y, DETECTOR_WIRE_NORMAL_MAGNITUDE, + HANDLE_LINE_WIDTH, LINE_HIT_HALF_WIDTH_PX, MIRROR_BACK_WIDTH, MIRROR_FRONT_WIDTH, @@ -50,8 +52,6 @@ import { import type { ViewOptionsModel } from "../ViewOptionsModel.js"; import { DetectorChartPanel } from "./DetectorChartPanel.js"; -const INITIAL_CHART_OFFSET_Y = 10; - export class DetectorView extends BaseOpticalElementView { public readonly bodyDragListener: RichDragListenerType; private readonly backPath: Path; @@ -64,7 +64,7 @@ export class DetectorView extends BaseOpticalElementView { private readonly chartPanel: DetectorChartPanel; /** View-space offset of the chart center from the detector midpoint. */ - private chartOffset = new Vector2(DETECTOR_INITIAL_CHART_OFFSET_X, INITIAL_CHART_OFFSET_Y); + private chartOffset = new Vector2(DETECTOR_INITIAL_CHART_OFFSET_X, DETECTOR_INITIAL_CHART_OFFSET_Y); // Properties driving the WireNode (updated imperatively in rebuild / chart drag). // Initialized with separated positions to avoid zero-length cubic bezier crash. @@ -101,7 +101,7 @@ export class DetectorView extends BaseOpticalElementView { }); this.ticksPath = new Path(null, { stroke: OpticsLabColors.detectorTickStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, lineCap: "round", pickable: false, }); diff --git a/src/common/view/edit-controls/EditControlHelpers.ts b/src/common/view/edit-controls/EditControlHelpers.ts index 7306278..08b4a39 100644 --- a/src/common/view/edit-controls/EditControlHelpers.ts +++ b/src/common/view/edit-controls/EditControlHelpers.ts @@ -14,6 +14,9 @@ import type { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../../i18n/StringManager.js"; import OpticsLabColors from "../../../OpticsLabColors.js"; import { + ELEMENT_ANGLE_MAX_DEG, + ELEMENT_ANGLE_MIN_DEG, + FONT_11PX, SEGMENT_LENGTH_MAX, SEGMENT_LENGTH_MIN, SLIDER_THUMB_HEIGHT, @@ -31,7 +34,7 @@ export type { EditControlsResult } from "./EditControlsResult.js"; export const SLIDER_TRACK_SIZE = new Dimension2(SLIDER_TRACK_WIDTH, SLIDER_TRACK_HEIGHT); export const SLIDER_THUMB_SIZE = new Dimension2(SLIDER_THUMB_WIDTH, SLIDER_THUMB_HEIGHT); -export const LABEL_FONT = "11px sans-serif"; +export const LABEL_FONT = FONT_11PX; // ── Private widget helpers ──────────────────────────────────────────────────── @@ -282,7 +285,7 @@ export function buildSegmentAngleControl( triggerRebuild: () => void, tandem: Tandem, ): { control: Node; refresh: () => void } { - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); const angleProp = new NumberProperty(segmentAngleDeg(element.p1, element.p2), { range: A_RANGE, tandem: tandem.createTandem("numberProperty"), @@ -342,7 +345,7 @@ export function buildDirectionAngleAboutP1Control( triggerRebuild: () => void, tandem: Tandem, ): { control: Node; refresh: () => void } { - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); const angleProp = new NumberProperty(segmentAngleDeg(element.p1, element.p2), { range: A_RANGE, tandem: tandem.createTandem("numberProperty"), @@ -411,7 +414,7 @@ export function buildDirectionAngleControl( triggerRebuild: () => void, tandem: Tandem, ): { control: Node; refresh: () => void } { - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); const angleProp = new NumberProperty(radiansToDisplayDeg(getDirectionRadians()), { range: A_RANGE, tandem: tandem.createTandem("numberProperty"), diff --git a/src/common/view/edit-controls/GlassEditControls.ts b/src/common/view/edit-controls/GlassEditControls.ts index ee5cc52..55dd47b 100644 --- a/src/common/view/edit-controls/GlassEditControls.ts +++ b/src/common/view/edit-controls/GlassEditControls.ts @@ -16,8 +16,19 @@ import { CONSTRAINED_CURVATURE_MAX, CONSTRAINED_CURVATURE_MIN, CONSTRAINED_LENS_RADIUS_MIN, + DIMENSIONAL_GLASS_HEIGHT_MAX_M, + DIMENSIONAL_GLASS_HEIGHT_MIN_M, + DIMENSIONAL_GLASS_SIZE_DELTA, + DIMENSIONAL_GLASS_WIDTH_MAX_M, + DIMENSIONAL_GLASS_WIDTH_MIN_M, + ELEMENT_ANGLE_MAX_DEG, + ELEMENT_ANGLE_MIN_DEG, FOCAL_LENGTH_MAX_M, FOCAL_LENGTH_MIN_M, + PRISM_SIZE_DELTA, + PRISM_SIZE_MAX_M, + PRISM_SIZE_MIN_M, + REFRACTIVE_INDEX_DELTA, REFRACTIVE_INDEX_MAX, REFRACTIVE_INDEX_MIN, SEGMENT_LENGTH_MAX, @@ -108,7 +119,7 @@ function buildSphericalLensAngleControl( triggerRebuild: () => void, ): { control: NumberControl; refresh: () => void } { const controlStrings = StringManager.getInstance().getControlStrings(); - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); let prevAngleDeg = segmentAngleDeg(element.p1, element.p2); const angleProp = new NumberProperty(prevAngleDeg, { range: A_RANGE, tandem: Tandem.OPTIONAL }); let angleDriving = false; @@ -149,7 +160,7 @@ function buildGlassPrismAngleControl( triggerRebuild: () => void, ): { control: NumberControl; refresh: () => void } { const controlStrings = StringManager.getInstance().getControlStrings(); - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); let prevAngleDeg = prismPathAngleDeg(path); const angleProp = new NumberProperty(prevAngleDeg, { range: A_RANGE, tandem: Tandem.OPTIONAL }); let angleDriving = false; @@ -187,7 +198,7 @@ function buildDimensionalGlassAngleControl( triggerRebuild: () => void, ): { control: NumberControl; refresh: () => void } { const controlStrings = StringManager.getInstance().getControlStrings(); - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); const initDeg = (((element.rotation * (180 / Math.PI)) % 360) + 360) % 360; const angleProp = new NumberProperty(initDeg, { range: A_RANGE, tandem: Tandem.OPTIONAL }); let angleDriving = false; @@ -412,7 +423,7 @@ export function buildSphericalLensControls( controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -578,7 +589,7 @@ export function buildSymmetricLensControls( controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -744,7 +755,7 @@ export function buildPlanoLensControls( controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -814,7 +825,7 @@ export function buildHalfPlaneGlassControls(element: HalfPlaneGlass, triggerRebu controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -840,7 +851,7 @@ export function buildRefractiveIndexControls(element: BaseGlass, triggerRebuild: controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -863,8 +874,8 @@ export function buildEquilateralPrismControls( makeControl( controlStrings.sizeStringProperty, element.size, - new Range(0.1, 2.0), - 0.05, + new Range(PRISM_SIZE_MIN_M, PRISM_SIZE_MAX_M), + PRISM_SIZE_DELTA, (v) => { element.setSize(v); }, @@ -876,7 +887,7 @@ export function buildEquilateralPrismControls( controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -896,8 +907,8 @@ export function buildRightAnglePrismControls(element: RightAnglePrism, triggerRe makeControl( controlStrings.legLengthStringProperty, element.legLength, - new Range(0.1, 2.0), - 0.05, + new Range(PRISM_SIZE_MIN_M, PRISM_SIZE_MAX_M), + PRISM_SIZE_DELTA, (v) => { element.setLegLength(v); }, @@ -909,7 +920,7 @@ export function buildRightAnglePrismControls(element: RightAnglePrism, triggerRe controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -929,8 +940,8 @@ export function buildPorroPrismControls(element: PorroPrism, triggerRebuild: () makeControl( controlStrings.legLengthStringProperty, element.legLength, - new Range(0.1, 2.0), - 0.05, + new Range(PRISM_SIZE_MIN_M, PRISM_SIZE_MAX_M), + PRISM_SIZE_DELTA, (v) => { element.setLegLength(v); }, @@ -942,7 +953,7 @@ export function buildPorroPrismControls(element: PorroPrism, triggerRebuild: () controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -962,8 +973,8 @@ export function buildSlabGlassControls(element: SlabGlass, triggerRebuild: () => makeControl( controlStrings.widthStringProperty, element.width, - new Range(0.1, 3.0), - 0.05, + new Range(DIMENSIONAL_GLASS_WIDTH_MIN_M, DIMENSIONAL_GLASS_WIDTH_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setWidth(v); }, @@ -973,8 +984,8 @@ export function buildSlabGlassControls(element: SlabGlass, triggerRebuild: () => makeControl( controlStrings.heightStringProperty, element.height, - new Range(0.1, 2.0), - 0.05, + new Range(DIMENSIONAL_GLASS_HEIGHT_MIN_M, DIMENSIONAL_GLASS_HEIGHT_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setHeight(v); }, @@ -986,7 +997,7 @@ export function buildSlabGlassControls(element: SlabGlass, triggerRebuild: () => controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -1009,8 +1020,8 @@ export function buildParallelogramPrismControls( makeControl( controlStrings.widthStringProperty, element.width, - new Range(0.1, 3.0), - 0.05, + new Range(DIMENSIONAL_GLASS_WIDTH_MIN_M, DIMENSIONAL_GLASS_WIDTH_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setWidth(v); }, @@ -1020,8 +1031,8 @@ export function buildParallelogramPrismControls( makeControl( controlStrings.heightStringProperty, element.height, - new Range(0.1, 2.0), - 0.05, + new Range(DIMENSIONAL_GLASS_HEIGHT_MIN_M, DIMENSIONAL_GLASS_HEIGHT_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setHeight(v); }, @@ -1033,7 +1044,7 @@ export function buildParallelogramPrismControls( controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -1053,8 +1064,8 @@ export function buildDovePrismControls(element: DovePrism, triggerRebuild: () => makeControl( controlStrings.widthStringProperty, element.width, - new Range(0.1, 3.0), - 0.05, + new Range(DIMENSIONAL_GLASS_WIDTH_MIN_M, DIMENSIONAL_GLASS_WIDTH_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setWidth(v); }, @@ -1064,8 +1075,8 @@ export function buildDovePrismControls(element: DovePrism, triggerRebuild: () => makeControl( controlStrings.heightStringProperty, element.height, - new Range(0.1, 2.0), - 0.05, + new Range(DIMENSIONAL_GLASS_HEIGHT_MIN_M, DIMENSIONAL_GLASS_HEIGHT_MAX_M), + DIMENSIONAL_GLASS_SIZE_DELTA, (v) => { element.setHeight(v); }, @@ -1077,7 +1088,7 @@ export function buildDovePrismControls(element: DovePrism, triggerRebuild: () => controlStrings.refractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, diff --git a/src/common/view/edit-controls/LightSourceEditControls.ts b/src/common/view/edit-controls/LightSourceEditControls.ts index 47ffb7f..893e542 100644 --- a/src/common/view/edit-controls/LightSourceEditControls.ts +++ b/src/common/view/edit-controls/LightSourceEditControls.ts @@ -9,6 +9,7 @@ import { Range } from "scenerystack/dot"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../../i18n/StringManager.js"; import { + BRIGHTNESS_CONTROL_DELTA, BRIGHTNESS_MAX, BRIGHTNESS_MIN, DIVERGENCE_MAX_DEG, @@ -51,7 +52,7 @@ export function buildArcLightSourceControls(element: ArcLightSource, triggerRebu controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, @@ -92,7 +93,7 @@ export function buildPointSourceControls(element: PointSourceElement, triggerReb controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, @@ -137,7 +138,7 @@ export function buildBeamSourceControls(element: BeamSource, triggerRebuild: () controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, @@ -184,7 +185,7 @@ export function buildDivergentBeamControls(element: DivergentBeam, triggerRebuil controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, @@ -232,7 +233,7 @@ export function buildSingleRaySourceControls(element: SingleRaySource, triggerRe controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, @@ -271,7 +272,7 @@ export function buildContinuousSpectrumSourceControls( controlStrings.brightnessStringProperty, element.brightness, new Range(BRIGHTNESS_MIN, BRIGHTNESS_MAX), - 0.05, + BRIGHTNESS_CONTROL_DELTA, (v) => { element.brightness = v; }, diff --git a/src/common/view/edit-controls/MirrorEditControls.ts b/src/common/view/edit-controls/MirrorEditControls.ts index 3a3fafd..1bc3023 100644 --- a/src/common/view/edit-controls/MirrorEditControls.ts +++ b/src/common/view/edit-controls/MirrorEditControls.ts @@ -17,13 +17,27 @@ import { ARC_MIRROR_CURVATURE_MIN, ARC_MIRROR_RADIUS_MAX, ARC_MIRROR_RADIUS_MIN, + BEAM_SPLITTER_TRANSMIT_DELTA, + BEAM_SPLITTER_TRANSMIT_MAX, + BEAM_SPLITTER_TRANSMIT_MIN, DETECTOR_BINS_MAX, DETECTOR_BINS_MIN, + ELEMENT_ANGLE_MAX_DEG, + ELEMENT_ANGLE_MIN_DEG, + FIBER_CORE_FRACTION_DELTA, + FIBER_CORE_FRACTION_MAX, + FIBER_CORE_FRACTION_MIN, FIBER_OPTIC_OUTER_RADIUS_MAX_M, FIBER_OPTIC_OUTER_RADIUS_MIN_M, FOCAL_LENGTH_MAX_M, FOCAL_LENGTH_MIN_M, + GRATING_DUTY_CYCLE_DELTA, + GRATING_DUTY_CYCLE_MAX, + GRATING_DUTY_CYCLE_MIN, + GRATING_LINES_DENSITY_MAX, + GRATING_LINES_DENSITY_MIN, LINES_DENSITY_CONTROL_DELTA, + REFRACTIVE_INDEX_DELTA, REFRACTIVE_INDEX_MAX, REFRACTIVE_INDEX_MIN, } from "../../../OpticsLabConstants.js"; @@ -321,7 +335,7 @@ export function buildSegmentControls( export function buildApertureControls(element: ApertureElement, triggerRebuild: () => void): EditControlsResult { const controlStrings = StringManager.getInstance().getControlStrings(); - const A_RANGE = new Range(0, 360); + const A_RANGE = new Range(ELEMENT_ANGLE_MIN_DEG, ELEMENT_ANGLE_MAX_DEG); // Build an angle control that rotates the aperture (p1/p2) while preserving // the fractional positions of the gap endpoints (p3/p4) along the line. @@ -383,7 +397,7 @@ export function buildGratingControls( makeControl( controlStrings.linesDensityStringProperty, element.linesDensity, - new Range(1, 2500), + new Range(GRATING_LINES_DENSITY_MIN, GRATING_LINES_DENSITY_MAX), LINES_DENSITY_CONTROL_DELTA, (v) => { element.linesDensity = v; @@ -394,8 +408,8 @@ export function buildGratingControls( makeControl( controlStrings.dutyCycleStringProperty, element.dutyCycle, - new Range(0.01, 0.99), - 0.01, + new Range(GRATING_DUTY_CYCLE_MIN, GRATING_DUTY_CYCLE_MAX), + GRATING_DUTY_CYCLE_DELTA, (v) => { element.dutyCycle = v; }, @@ -418,8 +432,8 @@ export function buildBeamSplitterControls( makeControl( controlStrings.transmissionRatioStringProperty, element.transRatio, - new Range(0, 1), - 0.05, + new Range(BEAM_SPLITTER_TRANSMIT_MIN, BEAM_SPLITTER_TRANSMIT_MAX), + BEAM_SPLITTER_TRANSMIT_DELTA, (v) => { element.transRatio = v; }, @@ -449,7 +463,7 @@ export function buildFiberOpticControls(element: FiberOpticElement, triggerRebui controlStrings.claddingRefractiveIndexStringProperty, element.refIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.refIndex = v; }, @@ -460,7 +474,7 @@ export function buildFiberOpticControls(element: FiberOpticElement, triggerRebui controlStrings.coreRefractiveIndexStringProperty, element.coreRefIndex, new Range(REFRACTIVE_INDEX_MIN, REFRACTIVE_INDEX_MAX), - 0.05, + REFRACTIVE_INDEX_DELTA, (v) => { element.coreRefIndex = v; }, @@ -470,8 +484,8 @@ export function buildFiberOpticControls(element: FiberOpticElement, triggerRebui const coreFractionControl = makeControl( controlStrings.coreFractionStringProperty, element.coreRadiusFraction, - new Range(0.05, 0.95), - 0.05, + new Range(FIBER_CORE_FRACTION_MIN, FIBER_CORE_FRACTION_MAX), + FIBER_CORE_FRACTION_DELTA, (v) => { element.coreRadiusFraction = v; }, diff --git a/src/common/view/glass/GlassView.ts b/src/common/view/glass/GlassView.ts index ad90ce0..f7b62d9 100644 --- a/src/common/view/glass/GlassView.ts +++ b/src/common/view/glass/GlassView.ts @@ -19,6 +19,7 @@ import { Tandem } from "scenerystack/tandem"; import OpticsLabColors, { glassFill } from "../../../OpticsLabColors.js"; import { GLASS_STROKE_WIDTH, + HANDLE_LINE_WIDTH, HANDLE_RADIUS, PRISM_EDGE_ADD_RADIUS, PRISM_VERTEX_REMOVE_RADIUS, @@ -204,9 +205,9 @@ export class GlassView extends BaseOpticalElementView { new Circle(PRISM_EDGE_ADD_RADIUS, { fill: OpticsLabColors.prismAddFillProperty, stroke: OpticsLabColors.prismAddStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, }), - new Path(plusShape, { stroke: OpticsLabColors.prismAddStrokeProperty, lineWidth: 1.5 }), + new Path(plusShape, { stroke: OpticsLabColors.prismAddStrokeProperty, lineWidth: HANDLE_LINE_WIDTH }), ], }); @@ -266,9 +267,9 @@ export class GlassView extends BaseOpticalElementView { new Circle(PRISM_VERTEX_REMOVE_RADIUS, { fill: OpticsLabColors.prismRemoveFillProperty, stroke: OpticsLabColors.prismRemoveStrokeProperty, - lineWidth: 1.5, + lineWidth: HANDLE_LINE_WIDTH, }), - new Path(xShape, { stroke: OpticsLabColors.prismRemoveStrokeProperty, lineWidth: 1.5 }), + new Path(xShape, { stroke: OpticsLabColors.prismRemoveStrokeProperty, lineWidth: HANDLE_LINE_WIDTH }), ], }); diff --git a/src/common/view/glass/PlanoLensView.ts b/src/common/view/glass/PlanoLensView.ts index b22ffc0..a8ccaa6 100644 --- a/src/common/view/glass/PlanoLensView.ts +++ b/src/common/view/glass/PlanoLensView.ts @@ -13,14 +13,15 @@ import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Tandem } from "scenerystack/tandem"; +import { LENS_APEX_MIN_OFFSET_M } from "../../../OpticsLabConstants.js"; import OpticsLabNamespace from "../../../OpticsLabNamespace.js"; + import type { GlassPathPoint } from "../../model/glass/Glass.js"; import type { SphericalLens } from "../../model/glass/SphericalLens.js"; import type { ViewOptionsModel } from "../ViewOptionsModel.js"; import { SphericalLensView } from "./SphericalLensView.js"; /** Minimum distance (model units) the apex must sit past the corner midpoint. */ -const APEX_MIN_OFFSET = 0.02; export class PlanoLensView extends SphericalLensView { /** @@ -28,7 +29,7 @@ export class PlanoLensView extends SphericalLensView { * along the optical axis: * −1 for PlanoConvexLens (r2 < 0, apex on +dp side → relProj > 0, reqSign = −r2Sign = +1) * +1 for PlanoConcaveLens (r2 > 0, apex on −dp side → relProj < 0, reqSign = −r2Sign = −1) - * Stored as −r2Sign so the clamp condition is simply relProj * apexReqSign >= APEX_MIN_OFFSET. + * Stored as −r2Sign so the clamp condition is simply relProj * apexReqSign >= LENS_APEX_MIN_OFFSET_M. */ private readonly apexReqSign: 1 | -1; @@ -86,10 +87,10 @@ export class PlanoLensView extends SphericalLensView { const midY = (v1.y + v3.y) / 2; const relProj = (v2.x - midX) * dpx + (v2.y - midY) * dpy; - if (relProj * this.apexReqSign < APEX_MIN_OFFSET) { + if (relProj * this.apexReqSign < LENS_APEX_MIN_OFFSET_M) { // Apex has crossed to the wrong side (or is too close to flat) — snap back. - v2.x = midX + APEX_MIN_OFFSET * this.apexReqSign * dpx; - v2.y = midY + APEX_MIN_OFFSET * this.apexReqSign * dpy; + v2.x = midX + LENS_APEX_MIN_OFFSET_M * this.apexReqSign * dpx; + v2.y = midY + LENS_APEX_MIN_OFFSET_M * this.apexReqSign * dpy; } this.rebuild(); diff --git a/src/common/view/glass/SymmetricLensView.ts b/src/common/view/glass/SymmetricLensView.ts index ff24467..2ec48cd 100644 --- a/src/common/view/glass/SymmetricLensView.ts +++ b/src/common/view/glass/SymmetricLensView.ts @@ -19,14 +19,15 @@ import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Tandem } from "scenerystack/tandem"; +import { LENS_APEX_MIN_OFFSET_M } from "../../../OpticsLabConstants.js"; import OpticsLabNamespace from "../../../OpticsLabNamespace.js"; + import type { GlassPathPoint } from "../../model/glass/Glass.js"; import type { SphericalLens } from "../../model/glass/SphericalLens.js"; import type { ViewOptionsModel } from "../ViewOptionsModel.js"; import { SphericalLensView } from "./SphericalLensView.js"; /** Minimum distance (model units) the apex must sit past the corner midpoint. */ -const APEX_MIN_OFFSET = 0.02; export class SymmetricLensView extends SphericalLensView { /** +1 for BiconvexLens (r1 > 0), −1 for BiconcaveLens (r1 < 0). */ @@ -78,9 +79,9 @@ export class SymmetricLensView extends SphericalLensView { const midY = (v0.y + v4.y) / 2; const relProj = (v5.x - midX) * dpx + (v5.y - midY) * dpy; const reqSign = (this.rSign === 1 ? -1 : 1) as 1 | -1; // −rSign - if (relProj * reqSign < APEX_MIN_OFFSET) { - v5.x = midX + APEX_MIN_OFFSET * reqSign * dpx; - v5.y = midY + APEX_MIN_OFFSET * reqSign * dpy; + if (relProj * reqSign < LENS_APEX_MIN_OFFSET_M) { + v5.x = midX + LENS_APEX_MIN_OFFSET_M * reqSign * dpx; + v5.y = midY + LENS_APEX_MIN_OFFSET_M * reqSign * dpy; } // Re-derive r1 from the (now-clamped) apex and mirror to r2. const { r1 } = this.lens.getDR1R2(); @@ -100,9 +101,9 @@ export class SymmetricLensView extends SphericalLensView { const midY = (v1.y + v3.y) / 2; const relProj = (v2.x - midX) * dpx + (v2.y - midY) * dpy; const reqSign = this.rSign; // +rSign - if (relProj * reqSign < APEX_MIN_OFFSET) { - v2.x = midX + APEX_MIN_OFFSET * reqSign * dpx; - v2.y = midY + APEX_MIN_OFFSET * reqSign * dpy; + if (relProj * reqSign < LENS_APEX_MIN_OFFSET_M) { + v2.x = midX + LENS_APEX_MIN_OFFSET_M * reqSign * dpx; + v2.y = midY + LENS_APEX_MIN_OFFSET_M * reqSign * dpy; } // Re-derive r2 from the (now-clamped) apex and mirror to r1. const { r2 } = this.lens.getDR1R2(); diff --git a/src/common/view/guides/TrackView.ts b/src/common/view/guides/TrackView.ts index a8e1ac7..ea4b307 100644 --- a/src/common/view/guides/TrackView.ts +++ b/src/common/view/guides/TrackView.ts @@ -11,6 +11,8 @@ import type { ModelViewTransform2 } from "scenerystack/phetcommon"; import { Path, type RichDragListener } from "scenerystack/scenery"; import type { Tandem } from "scenerystack/tandem"; import OpticsLabColors from "../../../OpticsLabColors.js"; +import { TRACK_LINE_DASH, TRACK_LINE_WIDTH } from "../../../OpticsLabConstants.js"; + import OpticsLabNamespace from "../../../OpticsLabNamespace.js"; import type { TrackElement } from "../../model/guides/TrackElement.js"; import { BaseOpticalElementView } from "../BaseOpticalElementView.js"; @@ -24,9 +26,6 @@ import { } from "../ViewHelpers.js"; import type { ViewOptionsModel } from "../ViewOptionsModel.js"; -const TRACK_LINE_WIDTH = 2; -const TRACK_LINE_DASH = [8, 4]; - export class TrackView extends BaseOpticalElementView { public readonly bodyDragListener: RichDragListener; private readonly trackPath: Path; diff --git a/src/common/view/mirrors/AperturedParabolicMirrorView.ts b/src/common/view/mirrors/AperturedParabolicMirrorView.ts index f327c95..039f220 100644 --- a/src/common/view/mirrors/AperturedParabolicMirrorView.ts +++ b/src/common/view/mirrors/AperturedParabolicMirrorView.ts @@ -14,6 +14,7 @@ import { type Circle, Path, type RichDragListener } from "scenerystack/scenery"; import type { Tandem } from "scenerystack/tandem"; import OpticsLabColors from "../../../OpticsLabColors.js"; import { + APERTURED_MIRROR_MAX_APERTURE_FRACTION, LINE_HIT_HALF_WIDTH_PX, MIRROR_BACK_WIDTH, MIRROR_FOCAL_MARKER_SIZE_M, @@ -246,7 +247,10 @@ export class AperturedParabolicMirrorView extends BaseOpticalElementView { const chordLen = distance(p1, p2); const halfAperture = chordLen / 2; - const effectiveHalfWidth = Math.min(this.mirror.apertureHalfWidth, halfAperture * 0.99); + const effectiveHalfWidth = Math.min( + this.mirror.apertureHalfWidth, + halfAperture * APERTURED_MIRROR_MAX_APERTURE_FRACTION, + ); const allPoints = this.mirror.computePoints(); diff --git a/src/preferences/opticsLabQueryParameters.ts b/src/preferences/opticsLabQueryParameters.ts index 55dc536..42ada7a 100644 --- a/src/preferences/opticsLabQueryParameters.ts +++ b/src/preferences/opticsLabQueryParameters.ts @@ -6,12 +6,17 @@ import { logGlobal } from "scenerystack/phet-core"; import { QueryStringMachine } from "scenerystack/query-string-machine"; import { DEFAULT_RAY_DENSITY, + GRID_SPACING_M, GRID_SPACING_MAX_M, GRID_SPACING_MIN_M, MAX_RAY_DEPTH_PROPERTY_MAX, MAX_RAY_DEPTH_PROPERTY_MIN, + QUERY_DEFAULT_MAX_RAY_DEPTH, RAY_DENSITY_MAX, RAY_DENSITY_MIN, + RAY_STUB_LENGTH_DEFAULT_PX, + RAY_STUB_LENGTH_MAX_PX, + RAY_STUB_LENGTH_MIN_PX, } from "../OpticsLabConstants.js"; import OpticsLabNamespace from "../OpticsLabNamespace.js"; @@ -31,7 +36,7 @@ const opticsLabQueryParameters = QueryStringMachine.getAll({ */ maximumLightRayDepth: { type: "number" as const, - defaultValue: 50, + defaultValue: QUERY_DEFAULT_MAX_RAY_DEPTH, public: true, isValidValue: (value: number) => Number.isInteger(value) && value >= MAX_RAY_DEPTH_PROPERTY_MIN && value <= MAX_RAY_DEPTH_PROPERTY_MAX, @@ -47,7 +52,7 @@ const opticsLabQueryParameters = QueryStringMachine.getAll({ // Spacing between major grid lines, in model metres (same range as preferences / scene grid size). gridSpacing: { type: "number" as const, - defaultValue: 1, + defaultValue: GRID_SPACING_M, public: true, isValidValue: (value: number) => value >= GRID_SPACING_MIN_M && value <= GRID_SPACING_MAX_M, }, @@ -106,9 +111,9 @@ const opticsLabQueryParameters = QueryStringMachine.getAll({ /** Length of ray stubs in view pixels when ray-stubs mode is active. */ rayStubLength: { type: "number" as const, - defaultValue: 50, + defaultValue: RAY_STUB_LENGTH_DEFAULT_PX, public: true, - isValidValue: (value: number) => value >= 10 && value <= 200, + isValidValue: (value: number) => value >= RAY_STUB_LENGTH_MIN_PX && value <= RAY_STUB_LENGTH_MAX_PX, }, /** Show the background grid at startup. */