scax-engine 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scax-engine.cjs +199 -40
- package/dist/scax-engine.js +199 -40
- package/dist/scax-engine.umd.js +199 -40
- package/dist/types/index.d.ts +1 -0
- package/dist/types/parameters/constants.d.ts +24 -0
- package/dist/types/parameters/eye/eyemodel-parameter.d.ts +5 -4
- package/dist/types/scax-engine.d.ts +17 -0
- package/dist/types/sturm/sturm.d.ts +16 -1
- package/package.json +1 -1
package/dist/scax-engine.cjs
CHANGED
|
@@ -6932,6 +6932,13 @@ const FRAUNHOFER_REFRACTIVE_INDICES = {
|
|
|
6932
6932
|
d: 1.406,
|
|
6933
6933
|
C: 1.405318,
|
|
6934
6934
|
},
|
|
6935
|
+
// Navarro 수정체 중심 굴절률(d=1.42)용 분산 스펙
|
|
6936
|
+
lens_navarro: {
|
|
6937
|
+
F: 1.421585,
|
|
6938
|
+
e: 1.420542,
|
|
6939
|
+
d: 1.420,
|
|
6940
|
+
C: 1.419318,
|
|
6941
|
+
},
|
|
6935
6942
|
lens_anterior: {
|
|
6936
6943
|
F: 1.387507,
|
|
6937
6944
|
e: 1.386516,
|
|
@@ -7022,6 +7029,19 @@ const DEFAULT_STURM_TOP2_MIN_GAP_MM = 0.0;
|
|
|
7022
7029
|
* Sturm 분석에서 Top2 선택 시 허용하는 최소 축 각도 차(도)
|
|
7023
7030
|
*/
|
|
7024
7031
|
const DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG = 45;
|
|
7032
|
+
/**
|
|
7033
|
+
* Sturm Top2 이각(J0,J45) phasor 내적 상한: cos(Δ₂θ) 이 이 값 이하일 때
|
|
7034
|
+
* 두 주경선을 직교(선초점 쌍)로 취급한다. -1에 가까울수록 엄격.
|
|
7035
|
+
*/
|
|
7036
|
+
const DEFAULT_STURM_TOP2_PHASOR_OPPOSITION_MAX_DOT = -0.86;
|
|
7037
|
+
/**
|
|
7038
|
+
* phasor 직교 판정 실패 시 완화 단계에서 쓰는 내적 상한
|
|
7039
|
+
*/
|
|
7040
|
+
const DEFAULT_STURM_TOP2_PHASOR_FALLBACK_MAX_DOT = -0.55;
|
|
7041
|
+
/**
|
|
7042
|
+
* 축 각·phasor 폴백 후에도 후보가 없을 때 허용하는 최소 각도 차(도)
|
|
7043
|
+
*/
|
|
7044
|
+
const DEFAULT_STURM_TOP2_AXIS_LAST_RESORT_MIN_GAP_DEG = 22;
|
|
7025
7045
|
/**
|
|
7026
7046
|
* 실효 난시량이 이 값(D) 이상이면 U/V 중간점을 CLC 근사 중심으로 우선 사용
|
|
7027
7047
|
*/
|
|
@@ -7030,6 +7050,11 @@ const DEFAULT_EFFECTIVE_CYLINDER_THRESHOLD_D = 0.125;
|
|
|
7030
7050
|
* Sturm z-scan 간격(mm)
|
|
7031
7051
|
*/
|
|
7032
7052
|
const DEFAULT_STURM_STEP_MM = 0.01;
|
|
7053
|
+
/**
|
|
7054
|
+
* Sturm 분석 슬라이스의 profile.at.z를 각막(앞)~망막+이 값(mm) 구간으로 한정할 때
|
|
7055
|
+
* 망막 표면 z보다 진행 방향 +z로 허용하는 여유.
|
|
7056
|
+
*/
|
|
7057
|
+
const DEFAULT_STURM_PROFILE_WORLD_Z_PAST_RETINA_MM = 4;
|
|
7033
7058
|
|
|
7034
7059
|
const DEFAULT_DIR = new Vector3(0, 0, 1);
|
|
7035
7060
|
function isFiniteVector3(v) {
|
|
@@ -7885,48 +7910,48 @@ GullstrandParameter.parameter = {
|
|
|
7885
7910
|
name: "cornea_anterior",
|
|
7886
7911
|
z: 0.0,
|
|
7887
7912
|
radius: 7.7,
|
|
7888
|
-
n_before:
|
|
7889
|
-
n_after:
|
|
7913
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.air,
|
|
7914
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7890
7915
|
},
|
|
7891
7916
|
{
|
|
7892
7917
|
type: "spherical",
|
|
7893
7918
|
name: "cornea_posterior",
|
|
7894
7919
|
z: 0.5,
|
|
7895
7920
|
radius: 6.8,
|
|
7896
|
-
n_before:
|
|
7897
|
-
n_after:
|
|
7921
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7922
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7898
7923
|
},
|
|
7899
7924
|
{
|
|
7900
7925
|
type: "spherical",
|
|
7901
7926
|
name: "lens_anterior",
|
|
7902
7927
|
z: 3.6,
|
|
7903
7928
|
radius: 10.0,
|
|
7904
|
-
n_before:
|
|
7905
|
-
n_after:
|
|
7929
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7930
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_anterior,
|
|
7906
7931
|
},
|
|
7907
7932
|
{
|
|
7908
7933
|
type: "spherical",
|
|
7909
7934
|
name: "lens_nucleus_anterior",
|
|
7910
7935
|
z: 4.146,
|
|
7911
7936
|
radius: 7.911,
|
|
7912
|
-
n_before:
|
|
7913
|
-
n_after:
|
|
7937
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_anterior,
|
|
7938
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_anterior,
|
|
7914
7939
|
},
|
|
7915
7940
|
{
|
|
7916
7941
|
type: "spherical",
|
|
7917
7942
|
name: "lens_nucleus_posterior",
|
|
7918
7943
|
z: 6.565,
|
|
7919
7944
|
radius: -5.76,
|
|
7920
|
-
n_before:
|
|
7921
|
-
n_after:
|
|
7945
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_anterior,
|
|
7946
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_posterior,
|
|
7922
7947
|
},
|
|
7923
7948
|
{
|
|
7924
7949
|
type: "spherical",
|
|
7925
7950
|
name: "lens_posterior",
|
|
7926
7951
|
z: 7.2,
|
|
7927
7952
|
radius: -6,
|
|
7928
|
-
n_before:
|
|
7929
|
-
n_after:
|
|
7953
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_posterior,
|
|
7954
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.vitreous,
|
|
7930
7955
|
},
|
|
7931
7956
|
{
|
|
7932
7957
|
type: "spherical-image",
|
|
@@ -7952,8 +7977,8 @@ NavarroParameter.parameter = {
|
|
|
7952
7977
|
z: 0.0,
|
|
7953
7978
|
radius: 7.72,
|
|
7954
7979
|
conic: -0.26,
|
|
7955
|
-
n_before:
|
|
7956
|
-
n_after:
|
|
7980
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.air,
|
|
7981
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7957
7982
|
},
|
|
7958
7983
|
{
|
|
7959
7984
|
type: "aspherical",
|
|
@@ -7961,8 +7986,8 @@ NavarroParameter.parameter = {
|
|
|
7961
7986
|
z: 0.55,
|
|
7962
7987
|
radius: 6.5,
|
|
7963
7988
|
conic: 0.0,
|
|
7964
|
-
n_before:
|
|
7965
|
-
n_after:
|
|
7989
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7990
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7966
7991
|
},
|
|
7967
7992
|
{
|
|
7968
7993
|
type: "aspherical",
|
|
@@ -7970,8 +7995,8 @@ NavarroParameter.parameter = {
|
|
|
7970
7995
|
z: 0.55 + 3.05,
|
|
7971
7996
|
radius: 10.2,
|
|
7972
7997
|
conic: -3.13,
|
|
7973
|
-
n_before:
|
|
7974
|
-
n_after:
|
|
7998
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7999
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_navarro,
|
|
7975
8000
|
},
|
|
7976
8001
|
{
|
|
7977
8002
|
type: "aspherical",
|
|
@@ -7979,8 +8004,8 @@ NavarroParameter.parameter = {
|
|
|
7979
8004
|
z: 0.55 + 3.05 + 4.0,
|
|
7980
8005
|
radius: -6,
|
|
7981
8006
|
conic: -1,
|
|
7982
|
-
n_before:
|
|
7983
|
-
n_after:
|
|
8007
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_navarro,
|
|
8008
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.vitreous,
|
|
7984
8009
|
},
|
|
7985
8010
|
{
|
|
7986
8011
|
type: "spherical-image",
|
|
@@ -8001,7 +8026,7 @@ class Sturm {
|
|
|
8001
8026
|
this.lastResult = null;
|
|
8002
8027
|
this.lineOrder = ["g", "F", "e", "d", "C", "r"];
|
|
8003
8028
|
}
|
|
8004
|
-
calculate(rays, effectiveCylinderD, axisReferenceRays) {
|
|
8029
|
+
calculate(rays, effectiveCylinderD, axisReferenceRays, profileWorldZBounds) {
|
|
8005
8030
|
const frame = this.analysisFrameFromRays(axisReferenceRays?.length ? axisReferenceRays : rays);
|
|
8006
8031
|
const depthRange = this.depthRangeFromRays(rays, frame);
|
|
8007
8032
|
const sturmSlices = this.collectSturmSlices(rays, frame, depthRange, DEFAULT_STURM_STEP_MM);
|
|
@@ -8010,7 +8035,7 @@ class Sturm {
|
|
|
8010
8035
|
const groupFrame = this.analysisFrameFromRays(group.rays, frame);
|
|
8011
8036
|
const groupDepthRange = this.depthRangeFromRays(group.rays, groupFrame);
|
|
8012
8037
|
const slices = this.collectSturmSlices(group.rays, groupFrame, groupDepthRange, DEFAULT_STURM_STEP_MM);
|
|
8013
|
-
const analysis = this.analyzeSturmSlices(slices, effectiveCylinderD);
|
|
8038
|
+
const analysis = this.analyzeSturmSlices(slices, effectiveCylinderD, profileWorldZBounds);
|
|
8014
8039
|
return {
|
|
8015
8040
|
line: group.line,
|
|
8016
8041
|
wavelength_nm: group.wavelength_nm,
|
|
@@ -8144,6 +8169,9 @@ class Sturm {
|
|
|
8144
8169
|
const lambdaMinor = Math.max(0, trace / 2 - root);
|
|
8145
8170
|
const thetaRad = 0.5 * Math.atan2(2 * sxy, sxx - syy);
|
|
8146
8171
|
const angleMajorDeg = ((thetaRad * 180) / Math.PI + 360) % 180;
|
|
8172
|
+
const twoThetaRad = 2 * thetaRad;
|
|
8173
|
+
const j0 = Math.cos(twoThetaRad);
|
|
8174
|
+
const j45 = Math.sin(twoThetaRad);
|
|
8147
8175
|
const majorDirection = frame.u.clone().multiplyScalar(Math.cos(thetaRad))
|
|
8148
8176
|
.add(frame.v.clone().multiplyScalar(Math.sin(thetaRad)))
|
|
8149
8177
|
.normalize();
|
|
@@ -8153,6 +8181,8 @@ class Sturm {
|
|
|
8153
8181
|
wMajor: Math.sqrt(lambdaMajor),
|
|
8154
8182
|
wMinor: Math.sqrt(lambdaMinor),
|
|
8155
8183
|
angleMajorDeg,
|
|
8184
|
+
j0,
|
|
8185
|
+
j45,
|
|
8156
8186
|
angleMinorDeg: (angleMajorDeg + 90) % 180,
|
|
8157
8187
|
majorDirection: {
|
|
8158
8188
|
x: majorDirection.x,
|
|
@@ -8190,6 +8220,84 @@ class Sturm {
|
|
|
8190
8220
|
const d = Math.abs((((a - b) % 180) + 180) % 180);
|
|
8191
8221
|
return Math.min(d, 180 - d);
|
|
8192
8222
|
}
|
|
8223
|
+
phasorDot(p, q) {
|
|
8224
|
+
return p.j0 * q.j0 + p.j45 * q.j45;
|
|
8225
|
+
}
|
|
8226
|
+
/**
|
|
8227
|
+
* 평탄도 순으로 정렬된 슬라이스에서 선초점 쌍(직교 주경선)에 해당하는 Top2를 고른다.
|
|
8228
|
+
* 주경선 각 대신 이각 phasor (cos 2θ, sin 2θ)로 직교를 판정하고, 실패 시 각도·전역 검색·깊이 폴백을 사용한다.
|
|
8229
|
+
*/
|
|
8230
|
+
pickFlattestSturmPair(sortedByFlatness, top2MinGapMm, allSlices) {
|
|
8231
|
+
if (sortedByFlatness.length === 0)
|
|
8232
|
+
return [];
|
|
8233
|
+
const first = sortedByFlatness[0];
|
|
8234
|
+
const top2MinAngleGapDeg = DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG;
|
|
8235
|
+
const phasorStrict = DEFAULT_STURM_TOP2_PHASOR_OPPOSITION_MAX_DOT;
|
|
8236
|
+
const phasorLoose = DEFAULT_STURM_TOP2_PHASOR_FALLBACK_MAX_DOT;
|
|
8237
|
+
const lastResortMinDeg = DEFAULT_STURM_TOP2_AXIS_LAST_RESORT_MIN_GAP_DEG;
|
|
8238
|
+
const depthOk = (c) => Math.abs(c.depth - first.depth) >= top2MinGapMm;
|
|
8239
|
+
const byPhasor = (pool, maxDot) => pool.find((c) => (c !== first
|
|
8240
|
+
&& depthOk(c)
|
|
8241
|
+
&& this.phasorDot(first.profile, c.profile) <= maxDot));
|
|
8242
|
+
let second = byPhasor(sortedByFlatness, phasorStrict)
|
|
8243
|
+
?? byPhasor(sortedByFlatness, phasorLoose);
|
|
8244
|
+
if (!second) {
|
|
8245
|
+
second = sortedByFlatness.find((c) => (c !== first
|
|
8246
|
+
&& depthOk(c)
|
|
8247
|
+
&& this.axisDiffDeg(c.profile.angleMajorDeg, first.profile.angleMajorDeg) >= top2MinAngleGapDeg));
|
|
8248
|
+
}
|
|
8249
|
+
if (!second) {
|
|
8250
|
+
let best = null;
|
|
8251
|
+
let bestAxDiff = -1;
|
|
8252
|
+
for (const c of sortedByFlatness) {
|
|
8253
|
+
if (c === first || !depthOk(c))
|
|
8254
|
+
continue;
|
|
8255
|
+
const axd = this.axisDiffDeg(c.profile.angleMajorDeg, first.profile.angleMajorDeg);
|
|
8256
|
+
if (axd > bestAxDiff) {
|
|
8257
|
+
bestAxDiff = axd;
|
|
8258
|
+
best = c;
|
|
8259
|
+
}
|
|
8260
|
+
}
|
|
8261
|
+
if (best && bestAxDiff >= lastResortMinDeg)
|
|
8262
|
+
second = best;
|
|
8263
|
+
}
|
|
8264
|
+
// 평탄도 정렬 풀만으로는 같은 phasor만 연속으로 나오는 경우가 있어, 전 스캔에서 가장 반대인 phasor를 고른다.
|
|
8265
|
+
if (!second) {
|
|
8266
|
+
let best = null;
|
|
8267
|
+
let bestDot = 1;
|
|
8268
|
+
for (const c of allSlices) {
|
|
8269
|
+
if (c === first || !depthOk(c))
|
|
8270
|
+
continue;
|
|
8271
|
+
const d = this.phasorDot(first.profile, c.profile);
|
|
8272
|
+
if (d < bestDot) {
|
|
8273
|
+
bestDot = d;
|
|
8274
|
+
best = c;
|
|
8275
|
+
}
|
|
8276
|
+
}
|
|
8277
|
+
if (best && bestDot < -0.08)
|
|
8278
|
+
second = best;
|
|
8279
|
+
}
|
|
8280
|
+
// 마지막: 가장 납작한 슬라이스와 깊이 차가 가장 큰 “비교적 납작한” 슬라이스 (선초점이 z로 분리될 때)
|
|
8281
|
+
if (!second) {
|
|
8282
|
+
const flatThreshold = first.ratio * 1.5 + 1e-6;
|
|
8283
|
+
let best = null;
|
|
8284
|
+
let bestDepthSpan = -1;
|
|
8285
|
+
for (const c of allSlices) {
|
|
8286
|
+
if (c === first || !depthOk(c))
|
|
8287
|
+
continue;
|
|
8288
|
+
if (c.ratio > flatThreshold)
|
|
8289
|
+
continue;
|
|
8290
|
+
const span = Math.abs(c.depth - first.depth);
|
|
8291
|
+
if (span > bestDepthSpan) {
|
|
8292
|
+
bestDepthSpan = span;
|
|
8293
|
+
best = c;
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
if (best)
|
|
8297
|
+
second = best;
|
|
8298
|
+
}
|
|
8299
|
+
return second ? [first, second] : [first];
|
|
8300
|
+
}
|
|
8193
8301
|
buildApproxCenter(flattestTop2, smallestEllipse, preferTop2Mid) {
|
|
8194
8302
|
if (flattestTop2.length <= 0)
|
|
8195
8303
|
return null;
|
|
@@ -8214,6 +8322,19 @@ class Sturm {
|
|
|
8214
8322
|
const first = flattestTop2[0];
|
|
8215
8323
|
return { x: first.profile.at.x, y: first.profile.at.y, z: first.z, mode: "top1-flat" };
|
|
8216
8324
|
}
|
|
8325
|
+
slicesForSturmAnalysis(sturmSlices, bounds) {
|
|
8326
|
+
if (!bounds)
|
|
8327
|
+
return sturmSlices;
|
|
8328
|
+
const zMin = bounds.zMin;
|
|
8329
|
+
const zMax = bounds.zMax;
|
|
8330
|
+
if (!Number.isFinite(zMin) || !Number.isFinite(zMax) || zMax < zMin)
|
|
8331
|
+
return sturmSlices;
|
|
8332
|
+
const bounded = sturmSlices.filter((s) => {
|
|
8333
|
+
const z = s.profile?.at?.z;
|
|
8334
|
+
return Number.isFinite(z) && z >= zMin && z <= zMax;
|
|
8335
|
+
});
|
|
8336
|
+
return bounded.length >= 2 ? bounded : sturmSlices;
|
|
8337
|
+
}
|
|
8217
8338
|
groupByFraunhoferLine(rays) {
|
|
8218
8339
|
const groups = new Map();
|
|
8219
8340
|
for (const ray of rays) {
|
|
@@ -8234,21 +8355,17 @@ class Sturm {
|
|
|
8234
8355
|
}
|
|
8235
8356
|
return [...groups.values()].sort((a, b) => this.lineOrder.indexOf(a.line) - this.lineOrder.indexOf(b.line));
|
|
8236
8357
|
}
|
|
8237
|
-
analyzeSturmSlices(sturmSlices, effectiveCylinderD) {
|
|
8358
|
+
analyzeSturmSlices(sturmSlices, effectiveCylinderD, profileWorldZBounds) {
|
|
8238
8359
|
const top2MinGapMm = DEFAULT_STURM_TOP2_MIN_GAP_MM;
|
|
8239
|
-
const top2MinAngleGapDeg = DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG;
|
|
8240
8360
|
const effectiveCylinderThresholdD = DEFAULT_EFFECTIVE_CYLINDER_THRESHOLD_D;
|
|
8241
8361
|
const preferTop2Mid = effectiveCylinderD >= effectiveCylinderThresholdD;
|
|
8242
|
-
const
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
&& this.axisDiffDeg(candidate.profile.angleMajorDeg, first.profile.angleMajorDeg) >= top2MinAngleGapDeg));
|
|
8248
|
-
flattestTop2 = second ? [first, second] : [first];
|
|
8249
|
-
}
|
|
8362
|
+
const slicesForAnalysis = this.slicesForSturmAnalysis(sturmSlices, profileWorldZBounds ?? null);
|
|
8363
|
+
const sortedByFlatness = [...slicesForAnalysis].sort((a, b) => a.ratio - b.ratio);
|
|
8364
|
+
const flattestTop2 = sortedByFlatness.length > 0
|
|
8365
|
+
? this.pickFlattestSturmPair(sortedByFlatness, top2MinGapMm, slicesForAnalysis)
|
|
8366
|
+
: [];
|
|
8250
8367
|
let smallestEllipse = null;
|
|
8251
|
-
for (const slice of
|
|
8368
|
+
for (const slice of slicesForAnalysis) {
|
|
8252
8369
|
if (!smallestEllipse || slice.size < smallestEllipse.size)
|
|
8253
8370
|
smallestEllipse = slice;
|
|
8254
8371
|
}
|
|
@@ -8872,8 +8989,10 @@ class SCAXEngineCore {
|
|
|
8872
8989
|
const eyeRotXDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.x);
|
|
8873
8990
|
const eyeRotYDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.y);
|
|
8874
8991
|
this.eyeTiltDeg = this.normalizeEyeTilt(eye?.tilt);
|
|
8875
|
-
|
|
8876
|
-
|
|
8992
|
+
// Apply eye deviation in the opposite direction so a lens with the same
|
|
8993
|
+
// prism/base prescription can optically compensate the eye deviation.
|
|
8994
|
+
const eyeEulerXDeg = eyeRotYDeg + this.eyeTiltDeg.x;
|
|
8995
|
+
const eyeEulerYDeg = (-eyeRotXDeg) + this.eyeTiltDeg.y;
|
|
8877
8996
|
this.eyeRotationQuaternion = new Quaternion().setFromEuler(new Euler((eyeEulerXDeg * Math.PI) / 180, (eyeEulerYDeg * Math.PI) / 180, 0, "XYZ"));
|
|
8878
8997
|
this.eyeRotationQuaternionInverse = this.eyeRotationQuaternion.clone().invert();
|
|
8879
8998
|
this.eyeRotationPivot = new Vector3(0, 0, SCAXEngineCore.EYE_ROTATION_PIVOT_FROM_CORNEA_MM);
|
|
@@ -9035,10 +9154,22 @@ class SCAXEngineCore {
|
|
|
9035
9154
|
* - x_deg/y_deg는 프리즘 회전량에 eye.tilt를 합산한 최종 렌더 회전량입니다.
|
|
9036
9155
|
*/
|
|
9037
9156
|
getEyeRotation() {
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9157
|
+
// Keep render rotation strictly aligned with the internal eye rotation used in ray tracing.
|
|
9158
|
+
// Internal eye-space Euler:
|
|
9159
|
+
// eyeEulerXDeg = eyeRotYDeg + tilt.x
|
|
9160
|
+
// eyeEulerYDeg = (-eyeRotXDeg) + tilt.y
|
|
9161
|
+
// UI mapping (applyEyeRenderRotation):
|
|
9162
|
+
// object.rotation.x = -y_deg
|
|
9163
|
+
// object.rotation.y = x_deg
|
|
9164
|
+
// so we expose:
|
|
9165
|
+
// x_deg = eyeEulerYDeg
|
|
9166
|
+
// y_deg = -eyeEulerXDeg
|
|
9167
|
+
const eyeRotXDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.x);
|
|
9168
|
+
const eyeRotYDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.y);
|
|
9169
|
+
const eyeEulerXDeg = eyeRotYDeg + this.eyeTiltDeg.x;
|
|
9170
|
+
const eyeEulerYDeg = (-eyeRotXDeg) + this.eyeTiltDeg.y;
|
|
9171
|
+
const xDeg = eyeEulerYDeg;
|
|
9172
|
+
const yDeg = -eyeEulerXDeg;
|
|
9042
9173
|
return {
|
|
9043
9174
|
x_deg: xDeg,
|
|
9044
9175
|
y_deg: yDeg,
|
|
@@ -9098,12 +9229,40 @@ class SCAXEngineCore {
|
|
|
9098
9229
|
this.tracedRays = traced;
|
|
9099
9230
|
return traced;
|
|
9100
9231
|
}
|
|
9232
|
+
/**
|
|
9233
|
+
* Sturm 선초/근사중심에 사용할 슬라이스 centroid(world z) 구간:
|
|
9234
|
+
* 각막 전면(cornea_anterior) 이상 ~ 망막(retina) + 여유 이하.
|
|
9235
|
+
*/
|
|
9236
|
+
sturmEyeProfileWorldZBounds() {
|
|
9237
|
+
let corneaAnteriorZ = Number.POSITIVE_INFINITY;
|
|
9238
|
+
let retinaZ = Number.NEGATIVE_INFINITY;
|
|
9239
|
+
for (const surface of this.surfaces) {
|
|
9240
|
+
const name = String(surface.name || "").toLowerCase();
|
|
9241
|
+
const z = Number(this.readSurfacePosition(surface)?.z);
|
|
9242
|
+
if (!Number.isFinite(z))
|
|
9243
|
+
continue;
|
|
9244
|
+
if (name.includes("cornea") && name.includes("anterior")) {
|
|
9245
|
+
corneaAnteriorZ = Math.min(corneaAnteriorZ, z);
|
|
9246
|
+
}
|
|
9247
|
+
if (name === "retina") {
|
|
9248
|
+
retinaZ = Math.max(retinaZ, z);
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9251
|
+
if (!Number.isFinite(corneaAnteriorZ) || !Number.isFinite(retinaZ))
|
|
9252
|
+
return undefined;
|
|
9253
|
+
if (retinaZ + 1e-9 < corneaAnteriorZ)
|
|
9254
|
+
return undefined;
|
|
9255
|
+
return {
|
|
9256
|
+
zMin: corneaAnteriorZ,
|
|
9257
|
+
zMax: retinaZ + DEFAULT_STURM_PROFILE_WORLD_Z_PAST_RETINA_MM,
|
|
9258
|
+
};
|
|
9259
|
+
}
|
|
9101
9260
|
/**
|
|
9102
9261
|
* 2) Sturm calculation 전용 함수
|
|
9103
9262
|
* traced ray 집합에서 z-scan 기반 Sturm 슬라이스/근사 중심을 계산합니다.
|
|
9104
9263
|
*/
|
|
9105
9264
|
sturmCalculation(rays = this.tracedRays) {
|
|
9106
|
-
this.lastSturmGapAnalysis = this.sturm.calculate(rays, this.effectiveCylinderFromOpticSurfaces(), this.lastSourceRaysForSturm);
|
|
9265
|
+
this.lastSturmGapAnalysis = this.sturm.calculate(rays, this.effectiveCylinderFromOpticSurfaces(), this.lastSourceRaysForSturm, this.sturmEyeProfileWorldZBounds() ?? null);
|
|
9107
9266
|
return this.lastSturmGapAnalysis;
|
|
9108
9267
|
}
|
|
9109
9268
|
/**
|