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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/common/ZodiacStripBackground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export class ZodiacStripBackground extends Node {
this.addChild(band);

const segW = width / 12;
for (let i = 0; i < 12; i++) {
const label = new Text(signStringProperties[i]!, {
signStringProperties.forEach((signStringProperty, i) => {
const label = new Text(signStringProperty, {
font: new PhetFont(9),
fill: SolarSystemModelsColors.zodiacLabelColorProperty,
maxWidth: segW - 4,
Expand All @@ -42,6 +42,6 @@ export class ZodiacStripBackground extends Node {
});
this.addChild(divider);
}
}
});
}
}
16 changes: 12 additions & 4 deletions src/configurations/model/ConfigurationsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,16 +569,24 @@ export class ConfigurationsModel implements TModel {
}
}

public applyPreset(id: 1 | 2, key: PlanetPresetKey): void {
/**
* Applies a planet preset's semimajor axis. Returns false (and leaves the
* preset index Properties untouched) when the preset would put both
* planets on the same orbit — that configuration has no synodic period,
* so the caller should revert the requesting UI element instead.
*/
public applyPreset(id: 1 | 2, key: PlanetPresetKey): boolean {
const a = PLANET_PRESETS[key];
this.setSemimajorAxis(id, a, false);
if (!this.setSemimajorAxis(id, a, false)) {
return false;
}
const idx = PRESET_KEYS.indexOf(key);
if (id === 1) {
const idx = PRESET_KEYS.indexOf(key);
this.preset1IndexProperty.value = idx >= 0 ? idx : 0;
} else {
const idx = PRESET_KEYS.indexOf(key);
this.preset2IndexProperty.value = idx >= 0 ? idx : 0;
}
return true;
}

public reset(): void {
Expand Down
15 changes: 8 additions & 7 deletions src/configurations/view/ConfigurationsControlPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,18 @@ export class ConfigurationsControlPanel extends SolarSystemModelsPanel {
accessibleName: a11y.controls.targetPlanetStringProperty,
});

// When preset index changes, apply preset
model.preset1IndexProperty.lazyLink((idx) => {
// When preset index changes, apply preset. Both planets landing on the same
// orbit has no synodic period, so revert the combo box if that's rejected.
model.preset1IndexProperty.lazyLink((idx, oldIdx) => {
const key = PRESET_KEYS[idx];
if (key !== undefined) {
model.applyPreset(1, key);
if (key !== undefined && !model.applyPreset(1, key)) {
model.preset1IndexProperty.value = oldIdx;
}
});
model.preset2IndexProperty.lazyLink((idx) => {
model.preset2IndexProperty.lazyLink((idx, oldIdx) => {
const key = PRESET_KEYS[idx];
if (key !== undefined) {
model.applyPreset(2, key);
if (key !== undefined && !model.applyPreset(2, key)) {
model.preset2IndexProperty.value = oldIdx;
}
});

Expand Down
115 changes: 69 additions & 46 deletions src/configurations/view/ConfigurationsElongationIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,104 @@ import { CONFIGURATIONS_ELONGATION_ARC_RADIUS } from "../../SolarSystemModelsCon
import type { ConfigurationsModel } from "../model/ConfigurationsModel.js";

export class ConfigurationsElongationIndicator extends Node {
private mvt: ModelViewTransform2;
private readonly model: ConfigurationsModel;
private readonly sunArrow: ArrowNode;
private readonly planetArrow: ArrowNode;
private readonly arcPath: Path;
private readonly elongLabel: Text;

public constructor(model: ConfigurationsModel, mvt: ModelViewTransform2) {
super({ visibleProperty: model.showElongationAngleProperty });

const sunArrow = new ArrowNode(0, 0, 1, 0, {
this.model = model;
this.mvt = mvt;

this.sunArrow = new ArrowNode(0, 0, 1, 0, {
stroke: null,
fill: SolarSystemModelsColors.elongationColorProperty,
headWidth: 8,
headHeight: 6,
tailWidth: 2,
});
const planetArrow = new ArrowNode(0, 0, 1, 0, {
this.planetArrow = new ArrowNode(0, 0, 1, 0, {
stroke: null,
fill: SolarSystemModelsColors.elongationColorProperty,
headWidth: 8,
headHeight: 6,
tailWidth: 2,
});
const arcPath = new Path(null, {
this.arcPath = new Path(null, {
stroke: SolarSystemModelsColors.elongationColorProperty,
lineWidth: 1.5,
});
const elongLabel = new Text("", {
this.elongLabel = new Text("", {
font: new PhetFont(11),
fill: SolarSystemModelsColors.elongationColorProperty,
});

this.addChild(arcPath);
this.addChild(sunArrow);
this.addChild(planetArrow);
this.addChild(elongLabel);
this.addChild(this.arcPath);
this.addChild(this.sunArrow);
this.addChild(this.planetArrow);
this.addChild(this.elongLabel);

Multilink.multilink(
[model.pos1Property, model.pos2Property, model.elongationDegProperty, model.elongationLabelProperty] as const,
(p1, p2, elongDeg, elongLabel_) => {
// Convert model positions to view
const vp1 = mvt.modelToViewPosition(p1);
const vp2 = mvt.modelToViewPosition(p2);
const vSun = mvt.modelToViewPosition(Vector2.ZERO);
(p1, p2, elongDeg, elongLabel_) => this.update(p1, p2, elongDeg, elongLabel_),
);
}

// Direction from p1 toward Sun (view)
const sunDir = Math.atan2(vSun.y - vp1.y, vSun.x - vp1.x);
// Direction from p1 toward p2 (view)
const planetDir = Math.atan2(vp2.y - vp1.y, vp2.x - vp1.x);
/** Called by the screen view when the orbit scale changes and a new transform is built. */
public setModelViewTransform(mvt: ModelViewTransform2): void {
this.mvt = mvt;
this.update(
this.model.pos1Property.value,
this.model.pos2Property.value,
this.model.elongationDegProperty.value,
this.model.elongationLabelProperty.value,
);
}

// Arrow endpoints — extend to edge of diagram
const arrowLen = 180;
sunArrow.setTailAndTip(vp1.x, vp1.y, vp1.x + arrowLen * Math.cos(sunDir), vp1.y + arrowLen * Math.sin(sunDir));
planetArrow.setTailAndTip(
vp1.x,
vp1.y,
vp1.x + arrowLen * Math.cos(planetDir),
vp1.y + arrowLen * Math.sin(planetDir),
);
private update(p1: Vector2, p2: Vector2, elongDeg: number, elongLabel_: string): void {
// Convert model positions to view
const vp1 = this.mvt.modelToViewPosition(p1);
const vp2 = this.mvt.modelToViewPosition(p2);
const vSun = this.mvt.modelToViewPosition(Vector2.ZERO);

// Arc — draw from sunDir to planetDir in the elongation direction
const arcShape = new Shape();
if (Math.abs(elongDeg) > 0.5) {
// AS: if elongationValue < 0 (East): drawArc from -sunDir to -planetDir
// In view coords (y-down), angles are negated vs model
// We draw CW or CCW depending on sign
const startAngle = sunDir;
const endAngle = planetDir;
// Determine sweep direction: elongDeg < 0 (East) means target is east of Sun
const anticlockwise = elongDeg > 0; // W = clockwise sweep, E = anticlockwise
arcShape.arc(vp1.x, vp1.y, CONFIGURATIONS_ELONGATION_ARC_RADIUS, startAngle, endAngle, anticlockwise);
}
arcPath.shape = arcShape;
// Direction from p1 toward Sun (view)
const sunDir = Math.atan2(vSun.y - vp1.y, vSun.x - vp1.x);
// Direction from p1 toward p2 (view)
const planetDir = Math.atan2(vp2.y - vp1.y, vp2.x - vp1.x);

// Label at midpoint angle
const midAngle = (sunDir + planetDir) / 2;
const labelR = CONFIGURATIONS_ELONGATION_ARC_RADIUS + 14;
elongLabel.string = `${Math.abs(elongDeg).toFixed(1)}° ${elongLabel_}`;
elongLabel.centerX = vp1.x + labelR * Math.cos(midAngle);
elongLabel.centerY = vp1.y + labelR * Math.sin(midAngle);
},
// Arrow endpoints — extend to edge of diagram
const arrowLen = 180;
this.sunArrow.setTailAndTip(vp1.x, vp1.y, vp1.x + arrowLen * Math.cos(sunDir), vp1.y + arrowLen * Math.sin(sunDir));
this.planetArrow.setTailAndTip(
vp1.x,
vp1.y,
vp1.x + arrowLen * Math.cos(planetDir),
vp1.y + arrowLen * Math.sin(planetDir),
);

// Arc — draw from sunDir to planetDir in the elongation direction
const arcShape = new Shape();
if (Math.abs(elongDeg) > 0.5) {
// AS: if elongationValue < 0 (East): drawArc from -sunDir to -planetDir
// In view coords (y-down), angles are negated vs model
// We draw CW or CCW depending on sign
const startAngle = sunDir;
const endAngle = planetDir;
// Determine sweep direction: elongDeg < 0 (East) means target is east of Sun
const anticlockwise = elongDeg > 0; // W = clockwise sweep, E = anticlockwise
arcShape.arc(vp1.x, vp1.y, CONFIGURATIONS_ELONGATION_ARC_RADIUS, startAngle, endAngle, anticlockwise);
}
this.arcPath.shape = arcShape;

// Label at midpoint angle
const midAngle = (sunDir + planetDir) / 2;
const labelR = CONFIGURATIONS_ELONGATION_ARC_RADIUS + 14;
this.elongLabel.string = `${Math.abs(elongDeg).toFixed(1)}° ${elongLabel_}`;
this.elongLabel.centerX = vp1.x + labelR * Math.cos(midAngle);
this.elongLabel.centerY = vp1.y + labelR * Math.sin(midAngle);
}
}
15 changes: 12 additions & 3 deletions src/configurations/view/ConfigurationsScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,19 @@ export class ConfigurationsScreenView extends ScreenView {
const updateOrbits = () => {
const a1 = model.semimajorAxis1Property.value;
const a2 = model.semimajorAxis2Property.value;
this.mvt = buildMvt(a1, a2);
// Mutate the existing mvt's matrix in place (rather than replacing the
// object) so CelestialBodyNode's internal closure — captured once at
// construction — keeps reading the current transform on every future
// position update, not just the one right after this call.
this.mvt.setMatrix(buildMvt(a1, a2).matrix);

// pos1Property/pos2Property may have already reacted to the axis change
// (and pushed stale-scale positions) before this listener ran, so
// explicitly re-sync the planet nodes and elongation indicator now.
observerNode.translation = this.mvt.modelToViewPosition(model.pos1Property.value);
targetNode.translation = this.mvt.modelToViewPosition(model.pos2Property.value);
elongationIndicator.setModelViewTransform(this.mvt);

// Update elongation indicator's MVT reference
// (It holds a closure over mvt, so we need to re-link — simplest: rebuild shape via update)
const center = this.mvt.modelToViewPosition(Vector2.ZERO);

const r1 = this.mvt.modelToViewDeltaX(a1);
Expand Down
6 changes: 1 addition & 5 deletions src/configurations/view/ConfigurationsTimeReadout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,9 @@ export class ConfigurationsTimeReadout extends SolarSystemModelsPanel {

const timeText = new Text(timeStringProperty, FONT_OPTS);
const synodicText = new Text(synodicStringProperty, FONT_OPTS);
const configText = new Text("", FONT_OPTS);
const configText = new Text(model.currentConfigurationProperty, FONT_OPTS);
const countdownText = new Text(countdownStringProperty, FONT_OPTS);

model.currentConfigurationProperty.link((cfg) => {
configText.string = cfg;
});

const content = new VBox({
children: [timeText, synodicText, configText, countdownText],
spacing: 4,
Expand Down
3 changes: 3 additions & 0 deletions src/configurations/view/ConfigurationsTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ export class ConfigurationsTimeline extends Node {
},
drag: (_event, listener) => {
const synodic = model.synodicPeriodProperty.value;
if (synodic <= 0) {
return;
}
const scale = CYCLE_HEIGHT_PX / synodic;
const deltaY = initY - listener.parentPoint.y;
const newTime = initTime + deltaY / scale;
Expand Down
6 changes: 3 additions & 3 deletions src/ptolemaic/view/PtolemaicScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,20 @@ export class PtolemaicScreenView extends ScreenView {
}

// ── Zodiac sign labels at sign centers (+15°) ─────────────────────────
for (let i = 0; i < 12; i++) {
ZODIAC_SIGN_PROPS.forEach((signStringProperty, i) => {
const angle = ((i + 0.5) * Math.PI) / 6; // 15°, 45°, 75°, ... (sign centers)
const vx = ORBIT_VIEW_CENTER_X + Math.cos(angle) * ZODIAC_LABEL_RADIUS;
// Inverted Y: positive y in model = up in view, so negate for angle
const vy = ORBIT_VIEW_CENTER_Y - Math.sin(angle) * ZODIAC_LABEL_RADIUS;
const label = new Text(ZODIAC_SIGN_PROPS[i]!, {
const label = new Text(signStringProperty, {
font: new PhetFont(10),
fill: SolarSystemModelsColors.zodiacLabelColorProperty,
maxWidth: ZODIAC_LABEL_MAX_WIDTH,
});
label.centerX = vx;
label.centerY = vy;
this.addChild(label);
}
});

// ── Path trail (behind orbit circles) ─────────────────────────────────
this.pathTrail = new PtolemaicPathTrail(model, mvt);
Expand Down
Loading