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