From d4480d37029b7a8506b9b1fce380025f847e0d27 Mon Sep 17 00:00:00 2001 From: Christophe Le Besnerais Date: Tue, 4 Jul 2023 17:15:36 +0200 Subject: [PATCH 1/5] better rendering --- src/app/scene/scene.component.ts | 206 ++++++++++-------- src/app/scene/scene.model.ts | 11 +- src/app/scene/scene.service.ts | 64 +++--- .../search-panel/search-panel.component.html | 10 +- .../search-panel/search-panel.component.scss | 2 +- .../search-panel/search-panel.component.ts | 18 +- .../search-panel/search-panel.service.ts | 4 +- 7 files changed, 176 insertions(+), 139 deletions(-) diff --git a/src/app/scene/scene.component.ts b/src/app/scene/scene.component.ts index 6230852..ac1d148 100644 --- a/src/app/scene/scene.component.ts +++ b/src/app/scene/scene.component.ts @@ -6,16 +6,15 @@ import { CelestialBody, OrbitPoint, Point, - LagrangePoint, LAGRANGE_POINT_I18N_KEY, RING_I18N_KEY, - COMPASS_TITLE_I18N_KEY + COMPASS_TITLE_I18N_KEY, LAGRANGE_POINT_TYPES, LagrangePointType } from './scene.model'; import {select} from 'd3-selection'; import {curveCardinalClosed, line} from 'd3-shape'; import {zoom, zoomIdentity, ZoomTransform} from 'd3-zoom'; import {range} from 'd3-array'; -import {PX_TO_KM, SceneService, SOLAR_SYSTEM_SIZE} from './scene.service'; +import {PX_TO_KM, PX_TO_KM_CLOSE, SceneService, SOLAR_SYSTEM_SIZE} from './scene.service'; import {HAS_SYMBOL, SOLAR_SYSTEM, SUN} from './data/SolarSystem.data'; import {SearchPanelService} from '../shell/search-panel/search-panel.service'; import {MERCURY} from './data/Mercury.data'; @@ -110,6 +109,7 @@ export class SceneComponent implements OnInit, AfterViewInit { private svgSelection: any; private groupBackgroundSelection: any; private groupZoomSelection: any; + private groupZoomCloseSelection: any; private groupForegroundSelection: any; private d3Zoom: any; private labelsPath: any; @@ -150,8 +150,8 @@ export class SceneComponent implements OnInit, AfterViewInit { this.deZoom(); } }); - this.searchPanelService.onLagrangePointSelected.subscribe(point => { - this.zoomToLagrangePoint(point); + this.searchPanelService.onLagrangePointSelected.subscribe(type => { + this.zoomToLagrangePoint(type); }); fromEvent(window, 'resize').pipe(throttleTime(300, undefined, { trailing: true })).subscribe(() => { @@ -165,11 +165,14 @@ export class SceneComponent implements OnInit, AfterViewInit { }); this.groupBackgroundSelection = this.svgSelection.append('g'); this.groupZoomSelection = this.svgSelection.append('g'); + this.groupZoomCloseSelection = this.svgSelection.append('g'); this.groupForegroundSelection = this.svgSelection.append('g'); this.initReticule(); - this.initOrbits(); - this.initCelestialBodies(); + this.initOrbits(true); + this.initOrbits(false); + this.initCelestialBodies(true); + this.initCelestialBodies(false); this.initZoom(); this.translateService.onLangChange.subscribe(() => { @@ -191,8 +194,10 @@ export class SceneComponent implements OnInit, AfterViewInit { }); this.initScale(); - this.initRings(); - this.initLagrangePoints(); + this.initRings(true); + this.initRings(false); + this.initLagrangePoints(true); + this.initLagrangePoints(false); } private handleParamId(id: string): void { @@ -203,10 +208,10 @@ export class SceneComponent implements OnInit, AfterViewInit { complete: () => this.select(body) }); } else { - const point = EARTH.lagrangePoints.find(p => p.type === id); - if (point) { - this.translateService.get(LAGRANGE_POINT_I18N_KEY + point.type).subscribe(() => { - this.zoomToLagrangePoint(point); + const lagrangePointType = LAGRANGE_POINT_TYPES.find(t => t === id); + if (lagrangePointType) { + this.translateService.get(LAGRANGE_POINT_I18N_KEY + lagrangePointType).subscribe(() => { + this.zoomToLagrangePoint(lagrangePointType); }); } } @@ -224,11 +229,13 @@ export class SceneComponent implements OnInit, AfterViewInit { this.transform = e.transform; this.groupZoomSelection.attr('transform', e.transform); + this.groupZoomCloseSelection.attr('transform', e.transform); this.initLabels(); if (!isPan) { this.initScale(); - this.initLagrangePoints(); + this.initLagrangePoints(true); + this.initLagrangePoints(false); } }); this.svgSelection.call(this.d3Zoom); @@ -274,48 +281,57 @@ export class SceneComponent implements OnInit, AfterViewInit { ); } - private initCelestialBodies(): void { - this.groupZoomSelection.selectAll('.celestial-body') - .data(SOLAR_SYSTEM, d => d.id) - .join( - enter => enter.append('circle') - .attr('id', body => body.id) - .attr('class', body => 'celestial-body ' + body.type + ' ' + body.id) - .attr('r', body => body.radius / PX_TO_KM) - .attr('cx', body => body.position.x) - .attr('cy', body => body.position.y) - .attr('transform', body => this.getRotationForLongitudeOfAscendingNode(body)) - .on('click', (event, d) => { - this.select(d); - event.stopPropagation(); - }) - ); + private initCelestialBodies(close: boolean): void { + const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + + group.selectAll('.celestial-body') + .data(SOLAR_SYSTEM, d => d.id) + .join( + enter => enter.append('circle') + .attr('id', body => body.id) + .attr('class', body => 'celestial-body ' + body.type + ' ' + body.id) + .attr('r', body => body.radius / scale) + .attr('cx', body => body.position.x / scale) + .attr('cy', body => body.position.y / scale) + .attr('transform', body => this.getRotationForLongitudeOfAscendingNode(body, close)) + .on('click', (event, d) => { + this.select(d); + event.stopPropagation(); + }) + ); } - private initRings(): void { + private initRings(close: boolean): void { + const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; + const ringsData = SOLAR_SYSTEM.reduce<{ ring: Ring, body: CelestialBody }[]>((result, body) => { const rings = body.rings ?? []; return result.concat(rings.map(ring => ({ ring, body }))); }, []); this.translateService.get(ringsData.map(d => d.ring.id + RING_I18N_KEY)).subscribe(translations => { - this.groupZoomSelection.selectAll('.ring').remove(); - this.groupZoomSelection.selectAll('.ring') - .data(ringsData, d => d.ring.id) - .join( - enter => enter.append('path') - .attr('id', d => d.ring.id) - .attr('class', 'celestial-body ring') - .attr('d', d => this.getRingPath(d)) - .attr('transform', d => this.getRotationForLongitudeOfAscendingNode(d.body)) - .append('title') - .html(d => translations[d.ring.id + RING_I18N_KEY]) - ); + group.selectAll('.ring').remove(); + group.selectAll('.ring') + .data(ringsData, d => d.ring.id) + .join( + enter => enter.append('path') + .attr('id', d => d.ring.id) + .attr('class', 'celestial-body ring') + .attr('d', d => this.getRingPath(d, close)) + .attr('transform', d => this.getRotationForLongitudeOfAscendingNode(d.body, close)) + .append('title') + .html(d => translations[d.ring.id + RING_I18N_KEY]) + ); }); } - private getRingPath(data: { body: CelestialBody, ring: Ring }): string { - const position = data.body.position; + private getRingPath(data: { body: CelestialBody, ring: Ring }, close: boolean): string { + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + const position = { + x: data.body.position.x / scale, + y: data.body.position.y / scale + }; let outerRadius = (data.ring.radius + data.ring.width); let innerRadius = data.ring.radius; @@ -326,8 +342,8 @@ export class SceneComponent implements OnInit, AfterViewInit { innerRadius = (overlappingRings[0].radius + overlappingRings[0].width); } - innerRadius = innerRadius / PX_TO_KM; - outerRadius = outerRadius / PX_TO_KM; + innerRadius = innerRadius / scale; + outerRadius = outerRadius / scale; // https://stackoverflow.com/a/42425397/990193 return `M ${position.x} ${position.y - outerRadius} @@ -340,26 +356,31 @@ export class SceneComponent implements OnInit, AfterViewInit { Z`; } - private initLagrangePoints(): void { - this.translateService.get(EARTH.lagrangePoints.map(p => LAGRANGE_POINT_I18N_KEY + p.type)).subscribe(translations => { - this.groupZoomSelection.selectAll('.lagrange-point').remove(); - this.groupZoomSelection.selectAll('.lagrange-point') - .data(EARTH.lagrangePoints, d => d.type) - .join( - enter => { - const g = enter.append('g').attr('class', p => 'lagrange-point lagrange-point-' + p.type); - const halfWidth = LAGRANGE_POINTS_WIDTH / (2 * this.transform.k); - g.append('path') - .attr('d', p => `M ${p.x - halfWidth} ${p.y - halfWidth} L ${p.x + halfWidth} ${p.y + halfWidth}`); - g.append('path') - .attr('d', p => `M ${p.x - halfWidth} ${p.y + halfWidth} L ${p.x + halfWidth} ${p.y - halfWidth}`); - g.append('title').html(p => translations[LAGRANGE_POINT_I18N_KEY + p.type]); - } - ); + private initLagrangePoints(close: boolean): void { + const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; + const lagrangePoints = this.sceneService.getEarthLagrangePoints(close); + + this.translateService.get(lagrangePoints.map(p => LAGRANGE_POINT_I18N_KEY + p.type)).subscribe(translations => { + group.selectAll('.lagrange-point').remove(); + group.selectAll('.lagrange-point') + .data(lagrangePoints, d => d.type) + .join( + enter => { + const g = enter.append('g').attr('class', p => 'lagrange-point lagrange-point-' + p.type); + const halfWidth = LAGRANGE_POINTS_WIDTH / (2 * this.transform.k); + g.append('path') + .attr('d', p => `M ${p.x - halfWidth} ${p.y - halfWidth} L ${p.x + halfWidth} ${p.y + halfWidth}`); + g.append('path') + .attr('d', p => `M ${p.x - halfWidth} ${p.y + halfWidth} L ${p.x + halfWidth} ${p.y - halfWidth}`); + g.append('title').html(p => translations[LAGRANGE_POINT_I18N_KEY + p.type]); + } + ); }); } - private initOrbits(): void { + private initOrbits(close: boolean): void { + const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; + // "big" orbits does not render well with ellipse, so we use a path instead. // On the contrary "small" orbit does not look good with path, so we use an // ellipse for everything with a semi major axis <= to ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD @@ -369,21 +390,21 @@ export class SceneComponent implements OnInit, AfterViewInit { .filter((body) => body.id !== 'sun' && body.semiMajorAxis <= ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD) .map((body) => ({ body, - orbit: this.sceneService.getOrbitEllipse(body) + orbit: this.sceneService.getOrbitEllipse(body, close) })); - this.groupZoomSelection.selectAll('.orbit-ellipse') - .data(smallOrbitsData, (d) => d.body.id) - .join( - enter => enter.append('ellipse') - .attr('id', (d) => 'orbit_' + d.body.id) - .attr('class', (d) => 'orbit-ellipse orbit orbit-' + d.body.type + ' orbit-' + d.body.id) - .attr('cx', (d) => d.orbit.cx) - .attr('cy', (d) => d.orbit.cy) - .attr('rx', (d) => d.orbit.rx) - .attr('ry', (d) => d.orbit.ry) - .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body)) - ); + group.selectAll('.orbit-ellipse') + .data(smallOrbitsData, (d) => d.body.id) + .join( + enter => enter.append('ellipse') + .attr('id', (d) => 'orbit_' + d.body.id) + .attr('class', (d) => 'orbit-ellipse orbit orbit-' + d.body.type + ' orbit-' + d.body.id) + .attr('cx', (d) => d.orbit.cx) + .attr('cy', (d) => d.orbit.cy) + .attr('rx', (d) => d.orbit.rx) + .attr('ry', (d) => d.orbit.ry) + .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body, close)) + ); // Path: const lineFn = line(p => p.x, p => p.y).curve(curveCardinalClosed.tension(1)); @@ -391,26 +412,27 @@ export class SceneComponent implements OnInit, AfterViewInit { .filter((body) => body.id !== 'sun' && body.semiMajorAxis > ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD) .map((body) => ({ body, - orbit: lineFn(this.sceneService.getOrbitPath(body, NB_POINTS_ORBIT)) + orbit: lineFn(this.sceneService.getOrbitPath(body, NB_POINTS_ORBIT, close)) })); - this.groupZoomSelection.selectAll('.orbit-path') - .data(largeOrbitsData, (d) => d.body.id) - .join( - enter => enter.append('path') - .attr('id', (d) => 'orbit_' + d.body.id) - .attr('class', (d) => 'orbit-path orbit orbit-' + d.body.type + ' orbit-' + d.body.id) - .attr('d', (d) => d.orbit) - .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body)) - ); + group.selectAll('.orbit-path') + .data(largeOrbitsData, (d) => d.body.id) + .join( + enter => enter.append('path') + .attr('id', (d) => 'orbit_' + d.body.id) + .attr('class', (d) => 'orbit-path orbit orbit-' + d.body.type + ' orbit-' + d.body.id) + .attr('d', (d) => d.orbit) + .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body, close)) + ); } - private getRotationForLongitudeOfAscendingNode(body: CelestialBody): string|null { + private getRotationForLongitudeOfAscendingNode(body: CelestialBody, close: boolean): string|null { if (body.longitudeOfAscendingNode && body.orbitBody) { // we negate the longitude of ascending node because the rotate function is clockwise: - return `rotate(${-body.longitudeOfAscendingNode}, ${body.orbitBody.position.x}, ${body.orbitBody.position.y})`; + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + return `rotate(${-body.longitudeOfAscendingNode}, ${body.orbitBody.position.x / scale}, ${body.orbitBody.position.y / scale})`; } else if (body.orbitBody) { - return this.getRotationForLongitudeOfAscendingNode(body.orbitBody); + return this.getRotationForLongitudeOfAscendingNode(body.orbitBody, close); } else { return null; } @@ -597,8 +619,8 @@ export class SceneComponent implements OnInit, AfterViewInit { return this.zoomTo(bbox, scale); } - private zoomToLagrangePoint(point: LagrangePoint): Observable { - const element: any = select('.lagrange-point-' + point.type).node(); + private zoomToLagrangePoint(type: LagrangePointType): Observable { + const element: any = select('.lagrange-point-' + type).node(); return this.zoomTo(element.getBBox(), ZOOM_EXTENT[1]); } diff --git a/src/app/scene/scene.model.ts b/src/app/scene/scene.model.ts index b692f63..228afb4 100644 --- a/src/app/scene/scene.model.ts +++ b/src/app/scene/scene.model.ts @@ -34,7 +34,7 @@ export interface Ring { } /** - * position: px + * position: km from the Sun * speed: km/s * mass: kg * radius: km @@ -59,7 +59,6 @@ export interface CelestialBody { type: CelestialBodyType; satellites: CelestialBody[]; orbitBody: CelestialBody | null; - lagrangePoints?: [ LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint ]; rings?: Ring[]; unknownData?: { speed?: boolean; @@ -81,6 +80,14 @@ export enum LagrangePointType { L5 = 'l5' } +export const LAGRANGE_POINT_TYPES: LagrangePointType[] = [ + LagrangePointType.L1, + LagrangePointType.L2, + LagrangePointType.L3, + LagrangePointType.L4, + LagrangePointType.L5 +]; + export interface LagrangePoint extends Point { type: LagrangePointType; } diff --git a/src/app/scene/scene.service.ts b/src/app/scene/scene.service.ts index 5c8b132..21cfdcd 100644 --- a/src/app/scene/scene.service.ts +++ b/src/app/scene/scene.service.ts @@ -12,6 +12,7 @@ import {EARTH} from './data/Earth.data'; * zoom! See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html */ export const PX_TO_KM = 1e5; +export const PX_TO_KM_CLOSE = 1e2; /** * in km @@ -30,47 +31,48 @@ export class SceneService { body.trueAnomaly = body.meanAnomaly; // TODO body.position = this.getPositionForTrueAnomaly(body, body.trueAnomaly); }); - - EARTH.lagrangePoints = this.getEarthLagrangePoints(); } /** * In px, relative to the sun at (0, 0) */ - public getOrbitEllipse(body: CelestialBody): Ellipse { + public getOrbitEllipse(body: CelestialBody, close: boolean): Ellipse { // convert eccentricity and semi major axis to radius and position using // https://en.wikipedia.org/wiki/Ellipse#Standard_equation + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; return { - cx: body.orbitBody.position.x - (body.eccentricity * body.semiMajorAxis / PX_TO_KM), - cy: body.orbitBody.position.y, - rx: body.semiMajorAxis / PX_TO_KM, - ry: Math.sqrt((body.semiMajorAxis ** 2) * (1 - (body.eccentricity ** 2))) / PX_TO_KM + cx: (body.orbitBody.position.x / scale) - (body.eccentricity * body.semiMajorAxis / scale), + cy: body.orbitBody.position.y / scale, + rx: body.semiMajorAxis / scale, + ry: Math.sqrt((body.semiMajorAxis ** 2) * (1 - (body.eccentricity ** 2))) / scale }; } /** * Positions in px, relative to the sun at (0, 0) */ - public getOrbitPath(body: CelestialBody, nbPoints = 360): OrbitPoint[] { + public getOrbitPath(body: CelestialBody, nbPoints: number, close: boolean): OrbitPoint[] { + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + const result = d3.range(0, 360, 360 / nbPoints).map(trueAnomaly => { const point = this.getPositionForTrueAnomaly(body, trueAnomaly); return { trueAnomaly, - x: point.x, - y: point.y + x: point.x / scale, + y: point.y / scale }; }); // add the body position to the orbit to make sure the orbit path will pass through the body: result.push({ trueAnomaly: body.trueAnomaly, - x: body.position.x, - y: body.position.y + x: body.position.x / scale, + y: body.position.y / scale }); return result.sort((p1, p2) => p1.trueAnomaly - p2.trueAnomaly); } /** - * in px, relative to the sun at (0, 0) + * in km, relative to the sun at (0, 0) */ public getPositionForTrueAnomaly(body: CelestialBody, trueAnomaly): Point { const d = this.getDistanceToFocusPoint(body, trueAnomaly); @@ -83,8 +85,8 @@ export class SceneService { // we have the position relative to the orbited body, so we add its // position to have the absolute position (to the sun) of the orbiting body : return { - x: (xKm / PX_TO_KM) + body.orbitBody.position.x, - y: (yKm / PX_TO_KM) + body.orbitBody.position.y + x: xKm + body.orbitBody.position.x, + y: yKm + body.orbitBody.position.y }; } @@ -99,42 +101,48 @@ export class SceneService { /** * https://en.wikipedia.org/wiki/Lagrange_point#Physical_and_mathematical_details - * @returns LagrangePoints the 5 Lagrange points for the earth and sun + * @returns LagrangePoints the 5 Lagrange points for the earth and sun (positions in px from the sun) */ - private getEarthLagrangePoints(): [ LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint ] { + public getEarthLagrangePoints(close: boolean): [ LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint ] { + const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + const earthPos = { + x: EARTH.position.x / scale, + y: EARTH.position.y / scale + }; + // Pythagore give the earth-sun distance - const distance = Math.sqrt((EARTH.position.x ** 2) + (EARTH.position.y ** 2)); + const distance = Math.sqrt((earthPos.x ** 2) + (earthPos.y ** 2)); // Thales give us l1, l2 and l3 from r and the earth position let r = distance * Math.cbrt(EARTH.mass / (3 * SUN.mass)); const l1: LagrangePoint = { - x: (EARTH.position.x * (distance - r)) / distance, - y: (EARTH.position.y * (distance - r)) / distance, + x: (earthPos.x * (distance - r)) / distance, + y: (earthPos.y * (distance - r)) / distance, type: LagrangePointType.L1 }; const l2: LagrangePoint = { - x: (EARTH.position.x * (distance + r)) / distance, - y: (EARTH.position.y * (distance + r)) / distance, + x: (earthPos.x * (distance + r)) / distance, + y: (earthPos.y * (distance + r)) / distance, type: LagrangePointType.L2 }; r = distance * ((7 * EARTH.mass) / (12 * SUN.mass)); const l3: LagrangePoint = { - x: - (EARTH.position.x * (distance - r)) / distance, - y: - (EARTH.position.y * (distance - r)) / distance, + x: - (earthPos.x * (distance - r)) / distance, + y: - (earthPos.y * (distance - r)) / distance, type: LagrangePointType.L3 }; // 60° rotation of the earth position give l4 const l4: LagrangePoint = { - x: (EARTH.position.x * Math.cos(60 * DEG_TO_RAD)) + (EARTH.position.y * Math.sin(60 * DEG_TO_RAD)), - y: - (EARTH.position.x * Math.sin(60 * DEG_TO_RAD)) + (EARTH.position.y * Math.cos(60 * DEG_TO_RAD)), + x: (earthPos.x * Math.cos(60 * DEG_TO_RAD)) + (earthPos.y * Math.sin(60 * DEG_TO_RAD)), + y: - (earthPos.x * Math.sin(60 * DEG_TO_RAD)) + (earthPos.y * Math.cos(60 * DEG_TO_RAD)), type: LagrangePointType.L4 }; // -60° rotation of the earth position give l5 const l5: LagrangePoint = { - x: (EARTH.position.x * Math.cos(-60 * DEG_TO_RAD)) + (EARTH.position.y * Math.sin(-60 * DEG_TO_RAD)), - y: - (EARTH.position.x * Math.sin(-60 * DEG_TO_RAD)) + (EARTH.position.y * Math.cos(-60 * DEG_TO_RAD)), + x: (earthPos.x * Math.cos(-60 * DEG_TO_RAD)) + (earthPos.y * Math.sin(-60 * DEG_TO_RAD)), + y: - (earthPos.x * Math.sin(-60 * DEG_TO_RAD)) + (earthPos.y * Math.cos(-60 * DEG_TO_RAD)), type: LagrangePointType.L5 }; diff --git a/src/app/shell/search-panel/search-panel.component.html b/src/app/shell/search-panel/search-panel.component.html index 8ad312d..6aef6d0 100644 --- a/src/app/shell/search-panel/search-panel.component.html +++ b/src/app/shell/search-panel/search-panel.component.html @@ -14,8 +14,8 @@ {{ body.id | translate }} - - {{ 'Sun–Earth ' + point.type | translate }} + + {{ 'Sun–Earth ' + type | translate }} @@ -119,9 +119,9 @@

- - - {{ 'Sun–Earth ' + point.type | translate }} + + + {{ 'Sun–Earth ' + type | translate }} diff --git a/src/app/shell/search-panel/search-panel.component.scss b/src/app/shell/search-panel/search-panel.component.scss index d72328a..871503a 100644 --- a/src/app/shell/search-panel/search-panel.component.scss +++ b/src/app/shell/search-panel/search-panel.component.scss @@ -123,6 +123,6 @@ mat-grid-tile.lagrange-point:hover { // https://github.com/angular/components/issues/11765#issuecomment-937377036 ::ng-deep .ng-animating mat-accordion mat-expansion-panel div.mat-expansion-panel-content { - height: 0px; + height: 0; visibility: hidden; } diff --git a/src/app/shell/search-panel/search-panel.component.ts b/src/app/shell/search-panel/search-panel.component.ts index 671274b..d6b74a8 100644 --- a/src/app/shell/search-panel/search-panel.component.ts +++ b/src/app/shell/search-panel/search-panel.component.ts @@ -1,10 +1,10 @@ import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; import { DWARF_PLANETS } from 'src/app/scene/data/DwarfPlanets.data'; import {HAS_SYMBOL, INNER_PLANETS, OUTER_PLANETS, SOLAR_SYSTEM, SUN} from 'src/app/scene/data/SolarSystem.data'; -import {CelestialBody, LAGRANGE_POINT_I18N_KEY, LagrangePoint} from '../../scene/scene.model'; +import {CelestialBody, LAGRANGE_POINT_I18N_KEY, LAGRANGE_POINT_TYPES, LagrangePointType} from '../../scene/scene.model'; import {SearchPanelService} from './search-panel.service'; import {GANYMEDE, JUPITER} from '../../scene/data/Jupiter.data'; -import {EARTH, MOON} from '../../scene/data/Earth.data'; +import {MOON} from '../../scene/data/Earth.data'; import {Subject} from 'rxjs'; import {debounceTime} from 'rxjs/operators'; import {TranslateService} from '@ngx-translate/core'; @@ -19,7 +19,7 @@ export class SearchPanelComponent implements OnInit, OnChanges { @Input() public search = ''; public searchResult: CelestialBody[] | null = null; - public searchResultLagrangePoints: LagrangePoint[] | null = null; + public searchResultLagrangePoints: LagrangePointType[] | null = null; public get nbCol(): number { return window.innerWidth <= 600 ? 2 : 4; @@ -29,10 +29,10 @@ export class SearchPanelComponent implements OnInit, OnChanges { public readonly INNER_PLANETS = INNER_PLANETS; public readonly OUTER_PLANETS = OUTER_PLANETS; public readonly DWARF_PLANETS = DWARF_PLANETS; - public readonly EARTH = EARTH; public readonly JUPITER = JUPITER; public readonly MOON = MOON; public readonly GANYMEDE = GANYMEDE; + public readonly LAGRANGE_POINT_TYPES = LAGRANGE_POINT_TYPES; public readonly HAS_SYMBOL = HAS_SYMBOL; public readonly NB_DWARF_PLANETS_SATELLITES = DWARF_PLANETS.reduce((nb, p) => nb + p.satellites.length, 0); @@ -63,8 +63,8 @@ export class SearchPanelComponent implements OnInit, OnChanges { this.searchService.onBodySelected.next(body); } - public onLagrangePointSelected(point: LagrangePoint): void { - this.searchService.onLagrangePointSelected.next(point); + public onLagrangePointSelected(type: LagrangePointType): void { + this.searchService.onLagrangePointSelected.next(type); } private onSearchChange(): void { @@ -73,9 +73,9 @@ export class SearchPanelComponent implements OnInit, OnChanges { this.searchResult = this.searchService.filter(data, [ 'translation' ], this.search).map(r => r.body); }); - this.translate.get(EARTH.lagrangePoints.map(p => LAGRANGE_POINT_I18N_KEY + p.type)).subscribe(translations => { - const data = EARTH.lagrangePoints.map(point => ({ point, translation: translations[LAGRANGE_POINT_I18N_KEY + point.type] })); - this.searchResultLagrangePoints = this.searchService.filter(data, [ 'translation' ], this.search).map(r => r.point); + this.translate.get(LAGRANGE_POINT_TYPES.map(t => LAGRANGE_POINT_I18N_KEY + t)).subscribe(translations => { + const data = LAGRANGE_POINT_TYPES.map(t => ({ type: t, translation: translations[LAGRANGE_POINT_I18N_KEY + t] })); + this.searchResultLagrangePoints = this.searchService.filter(data, [ 'translation' ], this.search).map(r => r.type); }); } diff --git a/src/app/shell/search-panel/search-panel.service.ts b/src/app/shell/search-panel/search-panel.service.ts index 9b5bc1f..9f33528 100644 --- a/src/app/shell/search-panel/search-panel.service.ts +++ b/src/app/shell/search-panel/search-panel.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import {Subject} from 'rxjs'; -import {CelestialBody, LagrangePoint} from '../../scene/scene.model'; +import {CelestialBody, LagrangePointType} from '../../scene/scene.model'; @Injectable({ providedIn: 'root' @@ -8,7 +8,7 @@ import {CelestialBody, LagrangePoint} from '../../scene/scene.model'; export class SearchPanelService { public onBodySelected: Subject = new Subject(); - public onLagrangePointSelected: Subject = new Subject(); + public onLagrangePointSelected: Subject = new Subject(); constructor() { } From 421f54830e7ac72cd7c0003e1bf10ee88a53279e Mon Sep 17 00:00:00 2001 From: Christophe Le Besnerais Date: Tue, 22 Aug 2023 15:35:58 +0200 Subject: [PATCH 2/5] better rendering --- src/app/scene/scene.component.scss | 20 ++++++++++++------- src/app/scene/scene.component.ts | 31 ++++++++++-------------------- src/app/scene/scene.service.ts | 2 +- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/app/scene/scene.component.scss b/src/app/scene/scene.component.scss index f21bfa1..c4908ed 100644 --- a/src/app/scene/scene.component.scss +++ b/src/app/scene/scene.component.scss @@ -21,23 +21,29 @@ :host ::ng-deep { - .hide-scale .scale, .hide-scale .compass { - display: none; + .hide-scale { + .scale, .compass { + display: none; + } } - .hide-reticule .reticuleV, .hide-reticule .reticuleH { - display: none; + .hide-reticule { + .reticuleV, .reticuleH { + display: none; + } } - .hide-labels .label-path, .hide-labels .label, .hide-labels .label-symbol { - display: none; + .hide-labels { + .label-path, .label, .label-symbol { + display: none; + } } .hide-orbits .orbit { display: none; } - .hide-orbits-satellites .orbit.satellite { + .hide-orbits-satellites .orbit-satellite { display: none; } diff --git a/src/app/scene/scene.component.ts b/src/app/scene/scene.component.ts index ac1d148..f707e95 100644 --- a/src/app/scene/scene.component.ts +++ b/src/app/scene/scene.component.ts @@ -169,9 +169,7 @@ export class SceneComponent implements OnInit, AfterViewInit { this.groupForegroundSelection = this.svgSelection.append('g'); this.initReticule(); - this.initOrbits(true); this.initOrbits(false); - this.initCelestialBodies(true); this.initCelestialBodies(false); this.initZoom(); @@ -194,9 +192,7 @@ export class SceneComponent implements OnInit, AfterViewInit { }); this.initScale(); - this.initRings(true); this.initRings(false); - this.initLagrangePoints(true); this.initLagrangePoints(false); } @@ -234,7 +230,6 @@ export class SceneComponent implements OnInit, AfterViewInit { this.initLabels(); if (!isPan) { this.initScale(); - this.initLagrangePoints(true); this.initLagrangePoints(false); } }); @@ -289,7 +284,6 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(SOLAR_SYSTEM, d => d.id) .join( enter => enter.append('circle') - .attr('id', body => body.id) .attr('class', body => 'celestial-body ' + body.type + ' ' + body.id) .attr('r', body => body.radius / scale) .attr('cx', body => body.position.x / scale) @@ -316,8 +310,7 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(ringsData, d => d.ring.id) .join( enter => enter.append('path') - .attr('id', d => d.ring.id) - .attr('class', 'celestial-body ring') + .attr('class', d => 'ring ' + d.ring.id) .attr('d', d => this.getRingPath(d, close)) .attr('transform', d => this.getRotationForLongitudeOfAscendingNode(d.body, close)) .append('title') @@ -397,7 +390,6 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(smallOrbitsData, (d) => d.body.id) .join( enter => enter.append('ellipse') - .attr('id', (d) => 'orbit_' + d.body.id) .attr('class', (d) => 'orbit-ellipse orbit orbit-' + d.body.type + ' orbit-' + d.body.id) .attr('cx', (d) => d.orbit.cx) .attr('cy', (d) => d.orbit.cy) @@ -419,7 +411,6 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(largeOrbitsData, (d) => d.body.id) .join( enter => enter.append('path') - .attr('id', (d) => 'orbit_' + d.body.id) .attr('class', (d) => 'orbit-path orbit orbit-' + d.body.type + ' orbit-' + d.body.id) .attr('d', (d) => d.orbit) .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body, close)) @@ -442,7 +433,7 @@ export class SceneComponent implements OnInit, AfterViewInit { const allLabelsData = SOLAR_SYSTEM.map(body => { return { body, - boundingBox: (select('#' + body.id).node() as any).getBoundingClientRect(), + boundingBox: (select('.' + body.id).node() as any).getBoundingClientRect(), visible: true, hasSymbol: HAS_SYMBOL.includes(body) }; @@ -485,13 +476,13 @@ export class SceneComponent implements OnInit, AfterViewInit { .transition() .duration(LABEL_TRANSITION_MS) .style('opacity', 1); - select('#orbit_' + d.body.id).classed('hovered', true); + select('.orbit-' + d.body.id).classed('hovered', true); }) .on('mouseout', (event, d) => { this.labelsPath.transition() .duration(LABEL_TRANSITION_MS) .style('opacity', 0); - select('#orbit_' + d.body.id).classed('hovered', false); + select('.orbit-' + d.body.id).classed('hovered', false); }) .on('mousedown', () => { this.labelsPath.style('opacity', 0); @@ -508,8 +499,7 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(d => [ d ], (d) => d.body.id) .join( enter => enter.append('text') - .attr('id', (d) => 'labeltext_' + d.body.id) - .attr('class', (d) => 'label ' + d.body.type + ' ' + d.body.id) + .attr('class', (d) => 'label label-' + d.body.type + ' label-' + d.body.id) .attr('dominant-baseline', 'central') .text((d) => this.bodiesLabels[d.body.id]) .attr('x', (d) => d.boundingBox.right + LABEL_DISTANCE_TO_BODY.x + (d.hasSymbol ? 1.2 * SYMBOL_SIZE : 0)) @@ -523,8 +513,7 @@ export class SceneComponent implements OnInit, AfterViewInit { .data(d => d.body !== SUN && d.hasSymbol ? [ d ] : [], (d) => d.body.id) .join( enter => enter.append('image') - .attr('id', (d) => 'labelsymbol_' + d.body.id) - .attr('class', (d) => 'label-symbol ' + d.body.type + ' ' + d.body.id) + .attr('class', (d) => 'label-symbol label-symbol-' + d.body.type + ' label-symbol-' + d.body.id) .attr('href', (d) => 'assets/symbols/' + d.body.id + '.svg') .attr('width', SYMBOL_SIZE) .attr('height', SYMBOL_SIZE) @@ -642,7 +631,7 @@ export class SceneComponent implements OnInit, AfterViewInit { * so we have to wrap the element into a group, get the bbox, and remove the group. */ private getBoundingBox(body: CelestialBody): DOMRect { - const element: any = select('#' + body.id).node(); + const element: any = select('.' + body.id).node(); const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); element.parentNode.appendChild(group); group.appendChild(element); @@ -696,9 +685,9 @@ export class SceneComponent implements OnInit, AfterViewInit { private select(body: CelestialBody): void { this.deselectAll(); - select('#labeltext_' + body.id).classed('selected', true); - select('#labelsymbol_' + body.id).classed('selected', true); - select('#orbit_' + body.id).classed('selected', true); + select('.label-' + body.id).classed('selected', true); + select('.label-symbol-' + body.id).classed('selected', true); + select('.orbit-' + body.id).classed('selected', true); if (this.celestialBodyDialogRef) { this.celestialBodyDialogRef.componentInstance.body = body; diff --git a/src/app/scene/scene.service.ts b/src/app/scene/scene.service.ts index 21cfdcd..aab122b 100644 --- a/src/app/scene/scene.service.ts +++ b/src/app/scene/scene.service.ts @@ -6,7 +6,7 @@ import {EARTH} from './data/Earth.data'; /** * SVG does not work well with big number, so we have to divide each value - * (in km) by this ratio before drawing. SCG also doesn't have much decimal + * (in km) by this ratio before drawing. SVG also doesn't have much decimal * precision, so we can't have a ratio too big, or small bodies won't render * properly. This does NOT take into account the scale applied by the current * zoom! See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html From 232a1865f3d1779b95e87de148c6803553bb24d7 Mon Sep 17 00:00:00 2001 From: Christophe Le Besnerais Date: Tue, 22 Aug 2023 15:52:12 +0200 Subject: [PATCH 3/5] better rendering --- src/app/scene/scene.component.ts | 182 +++++++++++++++---------------- src/app/scene/scene.service.ts | 20 +--- 2 files changed, 93 insertions(+), 109 deletions(-) diff --git a/src/app/scene/scene.component.ts b/src/app/scene/scene.component.ts index f707e95..30e6e9e 100644 --- a/src/app/scene/scene.component.ts +++ b/src/app/scene/scene.component.ts @@ -14,7 +14,7 @@ import {select} from 'd3-selection'; import {curveCardinalClosed, line} from 'd3-shape'; import {zoom, zoomIdentity, ZoomTransform} from 'd3-zoom'; import {range} from 'd3-array'; -import {PX_TO_KM, PX_TO_KM_CLOSE, SceneService, SOLAR_SYSTEM_SIZE} from './scene.service'; +import {SceneService, SOLAR_SYSTEM_SIZE} from './scene.service'; import {HAS_SYMBOL, SOLAR_SYSTEM, SUN} from './data/SolarSystem.data'; import {SearchPanelService} from '../shell/search-panel/search-panel.service'; import {MERCURY} from './data/Mercury.data'; @@ -81,6 +81,16 @@ const COMPAS_WIDTH = 35; // px const ZOOM_EXTENT: [ number, number ] = [ 0.00025, 200 ]; +/** + * SVG does not work well with big number, so we have to divide each value + * (in km) by this ratio before drawing. SVG also doesn't have much decimal + * precision, so we can't have a ratio too big, or small bodies won't render + * properly. This does NOT take into account the scale applied by the current + * zoom! See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html + */ +const PX_TO_KM = 1e5; +const PX_TO_KM_CLOSE = 1e2; + @Component({ selector: 'app-scene', templateUrl: './scene.component.html', @@ -109,13 +119,13 @@ export class SceneComponent implements OnInit, AfterViewInit { private svgSelection: any; private groupBackgroundSelection: any; private groupZoomSelection: any; - private groupZoomCloseSelection: any; private groupForegroundSelection: any; private d3Zoom: any; private labelsPath: any; private transform: ZoomTransform; private bodiesLabels = {}; private celestialBodyDialogRef: MatDialogRef<{ body: CelestialBody }>; + private scale = PX_TO_KM; // TODO PX_TO_KM_CLOSE private get center(): Point { return { @@ -165,12 +175,11 @@ export class SceneComponent implements OnInit, AfterViewInit { }); this.groupBackgroundSelection = this.svgSelection.append('g'); this.groupZoomSelection = this.svgSelection.append('g'); - this.groupZoomCloseSelection = this.svgSelection.append('g'); this.groupForegroundSelection = this.svgSelection.append('g'); this.initReticule(); - this.initOrbits(false); - this.initCelestialBodies(false); + this.initOrbits(); + this.initCelestialBodies(); this.initZoom(); this.translateService.onLangChange.subscribe(() => { @@ -192,8 +201,8 @@ export class SceneComponent implements OnInit, AfterViewInit { }); this.initScale(); - this.initRings(false); - this.initLagrangePoints(false); + this.initRings(); + this.initLagrangePoints(); } private handleParamId(id: string): void { @@ -225,12 +234,11 @@ export class SceneComponent implements OnInit, AfterViewInit { this.transform = e.transform; this.groupZoomSelection.attr('transform', e.transform); - this.groupZoomCloseSelection.attr('transform', e.transform); this.initLabels(); if (!isPan) { this.initScale(); - this.initLagrangePoints(false); + this.initLagrangePoints(); } }); this.svgSelection.call(this.d3Zoom); @@ -276,54 +284,48 @@ export class SceneComponent implements OnInit, AfterViewInit { ); } - private initCelestialBodies(close: boolean): void { - const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; - - group.selectAll('.celestial-body') - .data(SOLAR_SYSTEM, d => d.id) - .join( - enter => enter.append('circle') - .attr('class', body => 'celestial-body ' + body.type + ' ' + body.id) - .attr('r', body => body.radius / scale) - .attr('cx', body => body.position.x / scale) - .attr('cy', body => body.position.y / scale) - .attr('transform', body => this.getRotationForLongitudeOfAscendingNode(body, close)) - .on('click', (event, d) => { - this.select(d); - event.stopPropagation(); - }) - ); + private initCelestialBodies(): void { + this.groupZoomSelection.selectAll('.celestial-body') + .data(SOLAR_SYSTEM, d => d.id) + .join( + enter => enter.append('circle') + .attr('class', body => 'celestial-body ' + body.type + ' ' + body.id) + .attr('r', body => body.radius / this.scale) + .attr('cx', body => body.position.x / this.scale) + .attr('cy', body => body.position.y / this.scale) + .attr('transform', body => this.getRotationForLongitudeOfAscendingNode(body)) + .on('click', (event, d) => { + this.select(d); + event.stopPropagation(); + }) + ); } - private initRings(close: boolean): void { - const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; - + private initRings(): void { const ringsData = SOLAR_SYSTEM.reduce<{ ring: Ring, body: CelestialBody }[]>((result, body) => { const rings = body.rings ?? []; return result.concat(rings.map(ring => ({ ring, body }))); }, []); this.translateService.get(ringsData.map(d => d.ring.id + RING_I18N_KEY)).subscribe(translations => { - group.selectAll('.ring').remove(); - group.selectAll('.ring') - .data(ringsData, d => d.ring.id) - .join( - enter => enter.append('path') - .attr('class', d => 'ring ' + d.ring.id) - .attr('d', d => this.getRingPath(d, close)) - .attr('transform', d => this.getRotationForLongitudeOfAscendingNode(d.body, close)) - .append('title') - .html(d => translations[d.ring.id + RING_I18N_KEY]) - ); + this.groupZoomSelection.selectAll('.ring').remove(); + this.groupZoomSelection.selectAll('.ring') + .data(ringsData, d => d.ring.id) + .join( + enter => enter.append('path') + .attr('class', d => 'ring ' + d.ring.id) + .attr('d', d => this.getRingPath(d)) + .attr('transform', d => this.getRotationForLongitudeOfAscendingNode(d.body)) + .append('title') + .html(d => translations[d.ring.id + RING_I18N_KEY]) + ); }); } - private getRingPath(data: { body: CelestialBody, ring: Ring }, close: boolean): string { - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + private getRingPath(data: { body: CelestialBody, ring: Ring }): string { const position = { - x: data.body.position.x / scale, - y: data.body.position.y / scale + x: data.body.position.x / this.scale, + y: data.body.position.y / this.scale }; let outerRadius = (data.ring.radius + data.ring.width); let innerRadius = data.ring.radius; @@ -335,8 +337,8 @@ export class SceneComponent implements OnInit, AfterViewInit { innerRadius = (overlappingRings[0].radius + overlappingRings[0].width); } - innerRadius = innerRadius / scale; - outerRadius = outerRadius / scale; + innerRadius = innerRadius / this.scale; + outerRadius = outerRadius / this.scale; // https://stackoverflow.com/a/42425397/990193 return `M ${position.x} ${position.y - outerRadius} @@ -349,31 +351,28 @@ export class SceneComponent implements OnInit, AfterViewInit { Z`; } - private initLagrangePoints(close: boolean): void { - const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; - const lagrangePoints = this.sceneService.getEarthLagrangePoints(close); + private initLagrangePoints(): void { + const lagrangePoints = this.sceneService.getEarthLagrangePoints(this.scale); this.translateService.get(lagrangePoints.map(p => LAGRANGE_POINT_I18N_KEY + p.type)).subscribe(translations => { - group.selectAll('.lagrange-point').remove(); - group.selectAll('.lagrange-point') - .data(lagrangePoints, d => d.type) - .join( - enter => { - const g = enter.append('g').attr('class', p => 'lagrange-point lagrange-point-' + p.type); - const halfWidth = LAGRANGE_POINTS_WIDTH / (2 * this.transform.k); - g.append('path') - .attr('d', p => `M ${p.x - halfWidth} ${p.y - halfWidth} L ${p.x + halfWidth} ${p.y + halfWidth}`); - g.append('path') - .attr('d', p => `M ${p.x - halfWidth} ${p.y + halfWidth} L ${p.x + halfWidth} ${p.y - halfWidth}`); - g.append('title').html(p => translations[LAGRANGE_POINT_I18N_KEY + p.type]); - } - ); + this.groupZoomSelection.selectAll('.lagrange-point').remove(); + this.groupZoomSelection.selectAll('.lagrange-point') + .data(lagrangePoints, d => d.type) + .join( + enter => { + const g = enter.append('g').attr('class', p => 'lagrange-point lagrange-point-' + p.type); + const halfWidth = LAGRANGE_POINTS_WIDTH / (2 * this.transform.k); + g.append('path') + .attr('d', p => `M ${p.x - halfWidth} ${p.y - halfWidth} L ${p.x + halfWidth} ${p.y + halfWidth}`); + g.append('path') + .attr('d', p => `M ${p.x - halfWidth} ${p.y + halfWidth} L ${p.x + halfWidth} ${p.y - halfWidth}`); + g.append('title').html(p => translations[LAGRANGE_POINT_I18N_KEY + p.type]); + } + ); }); } - private initOrbits(close: boolean): void { - const group = close ? this.groupZoomCloseSelection : this.groupZoomSelection; - + private initOrbits(): void { // "big" orbits does not render well with ellipse, so we use a path instead. // On the contrary "small" orbit does not look good with path, so we use an // ellipse for everything with a semi major axis <= to ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD @@ -383,20 +382,20 @@ export class SceneComponent implements OnInit, AfterViewInit { .filter((body) => body.id !== 'sun' && body.semiMajorAxis <= ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD) .map((body) => ({ body, - orbit: this.sceneService.getOrbitEllipse(body, close) + orbit: this.sceneService.getOrbitEllipse(body, this.scale) })); - group.selectAll('.orbit-ellipse') - .data(smallOrbitsData, (d) => d.body.id) - .join( - enter => enter.append('ellipse') - .attr('class', (d) => 'orbit-ellipse orbit orbit-' + d.body.type + ' orbit-' + d.body.id) - .attr('cx', (d) => d.orbit.cx) - .attr('cy', (d) => d.orbit.cy) - .attr('rx', (d) => d.orbit.rx) - .attr('ry', (d) => d.orbit.ry) - .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body, close)) - ); + this.groupZoomSelection.selectAll('.orbit-ellipse') + .data(smallOrbitsData, (d) => d.body.id) + .join( + enter => enter.append('ellipse') + .attr('class', (d) => 'orbit-ellipse orbit orbit-' + d.body.type + ' orbit-' + d.body.id) + .attr('cx', (d) => d.orbit.cx) + .attr('cy', (d) => d.orbit.cy) + .attr('rx', (d) => d.orbit.rx) + .attr('ry', (d) => d.orbit.ry) + .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body)) + ); // Path: const lineFn = line(p => p.x, p => p.y).curve(curveCardinalClosed.tension(1)); @@ -404,26 +403,25 @@ export class SceneComponent implements OnInit, AfterViewInit { .filter((body) => body.id !== 'sun' && body.semiMajorAxis > ORBIT_SEMI_MAJOR_AXIS_ELLIPSE_THRESHOLD) .map((body) => ({ body, - orbit: lineFn(this.sceneService.getOrbitPath(body, NB_POINTS_ORBIT, close)) + orbit: lineFn(this.sceneService.getOrbitPath(body, NB_POINTS_ORBIT, this.scale)) })); - group.selectAll('.orbit-path') - .data(largeOrbitsData, (d) => d.body.id) - .join( - enter => enter.append('path') - .attr('class', (d) => 'orbit-path orbit orbit-' + d.body.type + ' orbit-' + d.body.id) - .attr('d', (d) => d.orbit) - .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body, close)) - ); + this.groupZoomSelection.selectAll('.orbit-path') + .data(largeOrbitsData, (d) => d.body.id) + .join( + enter => enter.append('path') + .attr('class', (d) => 'orbit-path orbit orbit-' + d.body.type + ' orbit-' + d.body.id) + .attr('d', (d) => d.orbit) + .attr('transform', (d) => this.getRotationForLongitudeOfAscendingNode(d.body)) + ); } - private getRotationForLongitudeOfAscendingNode(body: CelestialBody, close: boolean): string|null { + private getRotationForLongitudeOfAscendingNode(body: CelestialBody): string|null { if (body.longitudeOfAscendingNode && body.orbitBody) { // we negate the longitude of ascending node because the rotate function is clockwise: - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; - return `rotate(${-body.longitudeOfAscendingNode}, ${body.orbitBody.position.x / scale}, ${body.orbitBody.position.y / scale})`; + return `rotate(${-body.longitudeOfAscendingNode}, ${body.orbitBody.position.x / this.scale}, ${body.orbitBody.position.y / this.scale})`; } else if (body.orbitBody) { - return this.getRotationForLongitudeOfAscendingNode(body.orbitBody, close); + return this.getRotationForLongitudeOfAscendingNode(body.orbitBody); } else { return null; } diff --git a/src/app/scene/scene.service.ts b/src/app/scene/scene.service.ts index aab122b..97dbfe7 100644 --- a/src/app/scene/scene.service.ts +++ b/src/app/scene/scene.service.ts @@ -4,16 +4,6 @@ import * as d3 from 'd3'; import {SOLAR_SYSTEM, SUN} from './data/SolarSystem.data'; import {EARTH} from './data/Earth.data'; -/** - * SVG does not work well with big number, so we have to divide each value - * (in km) by this ratio before drawing. SVG also doesn't have much decimal - * precision, so we can't have a ratio too big, or small bodies won't render - * properly. This does NOT take into account the scale applied by the current - * zoom! See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html - */ -export const PX_TO_KM = 1e5; -export const PX_TO_KM_CLOSE = 1e2; - /** * in km */ @@ -36,10 +26,9 @@ export class SceneService { /** * In px, relative to the sun at (0, 0) */ - public getOrbitEllipse(body: CelestialBody, close: boolean): Ellipse { + public getOrbitEllipse(body: CelestialBody, scale: number): Ellipse { // convert eccentricity and semi major axis to radius and position using // https://en.wikipedia.org/wiki/Ellipse#Standard_equation - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; return { cx: (body.orbitBody.position.x / scale) - (body.eccentricity * body.semiMajorAxis / scale), cy: body.orbitBody.position.y / scale, @@ -51,9 +40,7 @@ export class SceneService { /** * Positions in px, relative to the sun at (0, 0) */ - public getOrbitPath(body: CelestialBody, nbPoints: number, close: boolean): OrbitPoint[] { - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; - + public getOrbitPath(body: CelestialBody, nbPoints: number, scale: number): OrbitPoint[] { const result = d3.range(0, 360, 360 / nbPoints).map(trueAnomaly => { const point = this.getPositionForTrueAnomaly(body, trueAnomaly); return { @@ -103,8 +90,7 @@ export class SceneService { * https://en.wikipedia.org/wiki/Lagrange_point#Physical_and_mathematical_details * @returns LagrangePoints the 5 Lagrange points for the earth and sun (positions in px from the sun) */ - public getEarthLagrangePoints(close: boolean): [ LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint ] { - const scale = close ? PX_TO_KM_CLOSE : PX_TO_KM; + public getEarthLagrangePoints(scale: number): [ LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint, LagrangePoint ] { const earthPos = { x: EARTH.position.x / scale, y: EARTH.position.y / scale From ea284252d6cf0c0f71d297585cfd2e21c101772e Mon Sep 17 00:00:00 2001 From: Christophe Le Besnerais Date: Wed, 13 Sep 2023 16:22:47 +0200 Subject: [PATCH 4/5] better rendering --- src/app/scene/scene.component.ts | 47 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/app/scene/scene.component.ts b/src/app/scene/scene.component.ts index 30e6e9e..8fbee8e 100644 --- a/src/app/scene/scene.component.ts +++ b/src/app/scene/scene.component.ts @@ -79,17 +79,7 @@ const SCALE_TITLE_PLURAL_KEY = 'NB_AU Astronomical Units = NB_KM km'; const COMPAS_WIDTH = 35; // px -const ZOOM_EXTENT: [ number, number ] = [ 0.00025, 200 ]; - -/** - * SVG does not work well with big number, so we have to divide each value - * (in km) by this ratio before drawing. SVG also doesn't have much decimal - * precision, so we can't have a ratio too big, or small bodies won't render - * properly. This does NOT take into account the scale applied by the current - * zoom! See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html - */ -const PX_TO_KM = 1e5; -const PX_TO_KM_CLOSE = 1e2; +const ZOOM_EXTENT: [ number, number ] = [ 0.0000005, 0.5 ]; @Component({ selector: 'app-scene', @@ -125,7 +115,16 @@ export class SceneComponent implements OnInit, AfterViewInit { private transform: ZoomTransform; private bodiesLabels = {}; private celestialBodyDialogRef: MatDialogRef<{ body: CelestialBody }>; - private scale = PX_TO_KM; // TODO PX_TO_KM_CLOSE + + /** + * SVG does not work well with big number, so we have to divide each value + * (in km) by this ratio before drawing (in px). SVG also doesn't have much + * decimal precision, so we can't have a ratio too big, or small bodies won't + * render properly. This does NOT take into account the scale applied by the + * current zoom! + * See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html + */ + private scale = 75; private get center(): Point { return { @@ -244,7 +243,7 @@ export class SceneComponent implements OnInit, AfterViewInit { this.svgSelection.call(this.d3Zoom); this.transform = zoomIdentity.translate(this.center.x, this.center.y) - .scale(Math.min(this.center.x * 2, this.center.y * 2) / (SOLAR_SYSTEM_SIZE / PX_TO_KM)); + .scale(Math.min(this.center.x * 2, this.center.y * 2) / (SOLAR_SYSTEM_SIZE / this.scale)); this.svgSelection.call(this.d3Zoom.transform, this.transform); } @@ -527,10 +526,10 @@ export class SceneComponent implements OnInit, AfterViewInit { const paddingY = window.innerWidth <= 400 ? 40 : 50; // px const averageScaleWidth = Math.min(200, window.innerWidth - paddingX - COMPAS_WIDTH - 200); // px - const scaleSizeAU = averageScaleWidth / ((AU_TO_KM / PX_TO_KM) * this.transform.k); // au + const scaleSizeAU = averageScaleWidth / ((AU_TO_KM / this.scale) * this.transform.k); // au // find the nearest available scale value: const scale = SCALE_POSSIBLE_VALUES.sort((a, b) => Math.abs(scaleSizeAU - a.max) - Math.abs(scaleSizeAU - b.max))[0]; - const scaleWidth = ((scale.max * AU_TO_KM) / PX_TO_KM) * this.transform.k; // px + const scaleWidth = ((scale.max * AU_TO_KM) / this.scale) * this.transform.k; // px const scaleSizeKm = Math.round(scale.max * AU_TO_KM); // km this.groupForegroundSelection.select('.scale').remove(); @@ -543,14 +542,14 @@ export class SceneComponent implements OnInit, AfterViewInit { // ticks for (let i = 0; i < scale.max; i = i + scale.tickInterval) { - const nbPx = ((i * AU_TO_KM) / PX_TO_KM) * this.transform.k; + const nbPx = ((i * AU_TO_KM) / this.scale) * this.transform.k; const height = (i % (SCALE_LARGE_TICK_STEP * scale.tickInterval) === 0 || i === scale.max ? SCALE_HEIGHT_LARGE_TICK : SCALE_HEIGHT_SMALL_TICK); groupScaleSelection.append('path') .attr('shape-rendering', 'crispEdges') .attr('d', `M ${paddingX + COMPAS_WIDTH + nbPx} ${window.innerHeight - paddingY - (height / 2)} L ${paddingX + COMPAS_WIDTH + nbPx} ${window.innerHeight - paddingY + (height / 2)}`); } // last tick (not included in the previous loop because of float rounding error) - const nbPxLastTick = ((scale.max * AU_TO_KM) / PX_TO_KM) * this.transform.k; + const nbPxLastTick = ((scale.max * AU_TO_KM) / this.scale) * this.transform.k; groupScaleSelection.append('path') .attr('shape-rendering', 'crispEdges') .attr('d', `M ${paddingX + COMPAS_WIDTH + nbPxLastTick} ${window.innerHeight - paddingY - (SCALE_HEIGHT_LARGE_TICK / 2)} L ${paddingX + COMPAS_WIDTH + nbPxLastTick} ${window.innerHeight - paddingY + (SCALE_HEIGHT_LARGE_TICK / 2)}`); @@ -642,7 +641,7 @@ export class SceneComponent implements OnInit, AfterViewInit { private deZoom(): void { const zoomTo = zoomIdentity.translate(this.center.x, this.center.y) - .scale(Math.min(this.center.x * 2, this.center.y * 2) / (SOLAR_SYSTEM_SIZE / PX_TO_KM)); + .scale(Math.min(this.center.x * 2, this.center.y * 2) / (SOLAR_SYSTEM_SIZE / this.scale)); this.svgSelection.transition() .duration(ZOOM_TRANSITION_MS) .call(this.d3Zoom.transform, zoomTo); @@ -652,22 +651,22 @@ export class SceneComponent implements OnInit, AfterViewInit { const max = ZOOM_EXTENT[1]; switch (body) { case SUN: - return 5.0; + return 0.01; case MERCURY: case VENUS: return max; case EARTH: - return 100; + return 0.05; case MARS: return max; case JUPITER: - return 1.3; + return 0.05; case SATURN: - return 1.5; + return 0.03; case URANUS: - return 2.0; + return 0.1; case NEPTUNE: - return 0.6; + return 0.15; default: if (body.type === CelestialBodyType.DWARF_PLANET) { return max; From efa3266e7fd2b3279ca3a71e1ef27ca98f8d7366 Mon Sep 17 00:00:00 2001 From: Christophe Le Besnerais Date: Fri, 3 Nov 2023 15:14:33 +0100 Subject: [PATCH 5/5] better rendering --- src/app/scene/scene.component.ts | 16 ++++++++-------- src/app/scene/scene.service.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/scene/scene.component.ts b/src/app/scene/scene.component.ts index 8fbee8e..a92c2df 100644 --- a/src/app/scene/scene.component.ts +++ b/src/app/scene/scene.component.ts @@ -79,7 +79,7 @@ const SCALE_TITLE_PLURAL_KEY = 'NB_AU Astronomical Units = NB_KM km'; const COMPAS_WIDTH = 35; // px -const ZOOM_EXTENT: [ number, number ] = [ 0.0000005, 0.5 ]; +const ZOOM_EXTENT: [ number, number ] = [ 0.00025, 200 ]; @Component({ selector: 'app-scene', @@ -124,7 +124,7 @@ export class SceneComponent implements OnInit, AfterViewInit { * current zoom! * See https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html */ - private scale = 75; + private scale = 1e5; private get center(): Point { return { @@ -651,22 +651,22 @@ export class SceneComponent implements OnInit, AfterViewInit { const max = ZOOM_EXTENT[1]; switch (body) { case SUN: - return 0.01; + return 5.0; case MERCURY: case VENUS: return max; case EARTH: - return 0.05; + return 100; case MARS: return max; case JUPITER: - return 0.05; + return 1.3; case SATURN: - return 0.03; + return 1.5; case URANUS: - return 0.1; + return 2.0; case NEPTUNE: - return 0.15; + return 0.6; default: if (body.type === CelestialBodyType.DWARF_PLANET) { return max; diff --git a/src/app/scene/scene.service.ts b/src/app/scene/scene.service.ts index 97dbfe7..27d2e06 100644 --- a/src/app/scene/scene.service.ts +++ b/src/app/scene/scene.service.ts @@ -30,7 +30,7 @@ export class SceneService { // convert eccentricity and semi major axis to radius and position using // https://en.wikipedia.org/wiki/Ellipse#Standard_equation return { - cx: (body.orbitBody.position.x / scale) - (body.eccentricity * body.semiMajorAxis / scale), + cx: (body.orbitBody.position.x / scale) - ((body.eccentricity * body.semiMajorAxis) / scale), cy: body.orbitBody.position.y / scale, rx: body.semiMajorAxis / scale, ry: Math.sqrt((body.semiMajorAxis ** 2) * (1 - (body.eccentricity ** 2))) / scale