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.umd.js
CHANGED
|
@@ -6936,6 +6936,13 @@
|
|
|
6936
6936
|
d: 1.406,
|
|
6937
6937
|
C: 1.405318,
|
|
6938
6938
|
},
|
|
6939
|
+
// Navarro 수정체 중심 굴절률(d=1.42)용 분산 스펙
|
|
6940
|
+
lens_navarro: {
|
|
6941
|
+
F: 1.421585,
|
|
6942
|
+
e: 1.420542,
|
|
6943
|
+
d: 1.420,
|
|
6944
|
+
C: 1.419318,
|
|
6945
|
+
},
|
|
6939
6946
|
lens_anterior: {
|
|
6940
6947
|
F: 1.387507,
|
|
6941
6948
|
e: 1.386516,
|
|
@@ -7026,6 +7033,19 @@
|
|
|
7026
7033
|
* Sturm 분석에서 Top2 선택 시 허용하는 최소 축 각도 차(도)
|
|
7027
7034
|
*/
|
|
7028
7035
|
const DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG = 45;
|
|
7036
|
+
/**
|
|
7037
|
+
* Sturm Top2 이각(J0,J45) phasor 내적 상한: cos(Δ₂θ) 이 이 값 이하일 때
|
|
7038
|
+
* 두 주경선을 직교(선초점 쌍)로 취급한다. -1에 가까울수록 엄격.
|
|
7039
|
+
*/
|
|
7040
|
+
const DEFAULT_STURM_TOP2_PHASOR_OPPOSITION_MAX_DOT = -0.86;
|
|
7041
|
+
/**
|
|
7042
|
+
* phasor 직교 판정 실패 시 완화 단계에서 쓰는 내적 상한
|
|
7043
|
+
*/
|
|
7044
|
+
const DEFAULT_STURM_TOP2_PHASOR_FALLBACK_MAX_DOT = -0.55;
|
|
7045
|
+
/**
|
|
7046
|
+
* 축 각·phasor 폴백 후에도 후보가 없을 때 허용하는 최소 각도 차(도)
|
|
7047
|
+
*/
|
|
7048
|
+
const DEFAULT_STURM_TOP2_AXIS_LAST_RESORT_MIN_GAP_DEG = 22;
|
|
7029
7049
|
/**
|
|
7030
7050
|
* 실효 난시량이 이 값(D) 이상이면 U/V 중간점을 CLC 근사 중심으로 우선 사용
|
|
7031
7051
|
*/
|
|
@@ -7034,6 +7054,11 @@
|
|
|
7034
7054
|
* Sturm z-scan 간격(mm)
|
|
7035
7055
|
*/
|
|
7036
7056
|
const DEFAULT_STURM_STEP_MM = 0.01;
|
|
7057
|
+
/**
|
|
7058
|
+
* Sturm 분석 슬라이스의 profile.at.z를 각막(앞)~망막+이 값(mm) 구간으로 한정할 때
|
|
7059
|
+
* 망막 표면 z보다 진행 방향 +z로 허용하는 여유.
|
|
7060
|
+
*/
|
|
7061
|
+
const DEFAULT_STURM_PROFILE_WORLD_Z_PAST_RETINA_MM = 4;
|
|
7037
7062
|
|
|
7038
7063
|
const DEFAULT_DIR = new Vector3(0, 0, 1);
|
|
7039
7064
|
function isFiniteVector3(v) {
|
|
@@ -7889,48 +7914,48 @@
|
|
|
7889
7914
|
name: "cornea_anterior",
|
|
7890
7915
|
z: 0.0,
|
|
7891
7916
|
radius: 7.7,
|
|
7892
|
-
n_before:
|
|
7893
|
-
n_after:
|
|
7917
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.air,
|
|
7918
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7894
7919
|
},
|
|
7895
7920
|
{
|
|
7896
7921
|
type: "spherical",
|
|
7897
7922
|
name: "cornea_posterior",
|
|
7898
7923
|
z: 0.5,
|
|
7899
7924
|
radius: 6.8,
|
|
7900
|
-
n_before:
|
|
7901
|
-
n_after:
|
|
7925
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7926
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7902
7927
|
},
|
|
7903
7928
|
{
|
|
7904
7929
|
type: "spherical",
|
|
7905
7930
|
name: "lens_anterior",
|
|
7906
7931
|
z: 3.6,
|
|
7907
7932
|
radius: 10.0,
|
|
7908
|
-
n_before:
|
|
7909
|
-
n_after:
|
|
7933
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7934
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_anterior,
|
|
7910
7935
|
},
|
|
7911
7936
|
{
|
|
7912
7937
|
type: "spherical",
|
|
7913
7938
|
name: "lens_nucleus_anterior",
|
|
7914
7939
|
z: 4.146,
|
|
7915
7940
|
radius: 7.911,
|
|
7916
|
-
n_before:
|
|
7917
|
-
n_after:
|
|
7941
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_anterior,
|
|
7942
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_anterior,
|
|
7918
7943
|
},
|
|
7919
7944
|
{
|
|
7920
7945
|
type: "spherical",
|
|
7921
7946
|
name: "lens_nucleus_posterior",
|
|
7922
7947
|
z: 6.565,
|
|
7923
7948
|
radius: -5.76,
|
|
7924
|
-
n_before:
|
|
7925
|
-
n_after:
|
|
7949
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_anterior,
|
|
7950
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_posterior,
|
|
7926
7951
|
},
|
|
7927
7952
|
{
|
|
7928
7953
|
type: "spherical",
|
|
7929
7954
|
name: "lens_posterior",
|
|
7930
7955
|
z: 7.2,
|
|
7931
7956
|
radius: -6,
|
|
7932
|
-
n_before:
|
|
7933
|
-
n_after:
|
|
7957
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_nucleus_posterior,
|
|
7958
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.vitreous,
|
|
7934
7959
|
},
|
|
7935
7960
|
{
|
|
7936
7961
|
type: "spherical-image",
|
|
@@ -7956,8 +7981,8 @@
|
|
|
7956
7981
|
z: 0.0,
|
|
7957
7982
|
radius: 7.72,
|
|
7958
7983
|
conic: -0.26,
|
|
7959
|
-
n_before:
|
|
7960
|
-
n_after:
|
|
7984
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.air,
|
|
7985
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7961
7986
|
},
|
|
7962
7987
|
{
|
|
7963
7988
|
type: "aspherical",
|
|
@@ -7965,8 +7990,8 @@
|
|
|
7965
7990
|
z: 0.55,
|
|
7966
7991
|
radius: 6.5,
|
|
7967
7992
|
conic: 0.0,
|
|
7968
|
-
n_before:
|
|
7969
|
-
n_after:
|
|
7993
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.cornea,
|
|
7994
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
7970
7995
|
},
|
|
7971
7996
|
{
|
|
7972
7997
|
type: "aspherical",
|
|
@@ -7974,8 +7999,8 @@
|
|
|
7974
7999
|
z: 0.55 + 3.05,
|
|
7975
8000
|
radius: 10.2,
|
|
7976
8001
|
conic: -3.13,
|
|
7977
|
-
n_before:
|
|
7978
|
-
n_after:
|
|
8002
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.aqueous,
|
|
8003
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.lens_navarro,
|
|
7979
8004
|
},
|
|
7980
8005
|
{
|
|
7981
8006
|
type: "aspherical",
|
|
@@ -7983,8 +8008,8 @@
|
|
|
7983
8008
|
z: 0.55 + 3.05 + 4.0,
|
|
7984
8009
|
radius: -6,
|
|
7985
8010
|
conic: -1,
|
|
7986
|
-
n_before:
|
|
7987
|
-
n_after:
|
|
8011
|
+
n_before: FRAUNHOFER_REFRACTIVE_INDICES.lens_navarro,
|
|
8012
|
+
n_after: FRAUNHOFER_REFRACTIVE_INDICES.vitreous,
|
|
7988
8013
|
},
|
|
7989
8014
|
{
|
|
7990
8015
|
type: "spherical-image",
|
|
@@ -8005,7 +8030,7 @@
|
|
|
8005
8030
|
this.lastResult = null;
|
|
8006
8031
|
this.lineOrder = ["g", "F", "e", "d", "C", "r"];
|
|
8007
8032
|
}
|
|
8008
|
-
calculate(rays, effectiveCylinderD, axisReferenceRays) {
|
|
8033
|
+
calculate(rays, effectiveCylinderD, axisReferenceRays, profileWorldZBounds) {
|
|
8009
8034
|
const frame = this.analysisFrameFromRays(axisReferenceRays?.length ? axisReferenceRays : rays);
|
|
8010
8035
|
const depthRange = this.depthRangeFromRays(rays, frame);
|
|
8011
8036
|
const sturmSlices = this.collectSturmSlices(rays, frame, depthRange, DEFAULT_STURM_STEP_MM);
|
|
@@ -8014,7 +8039,7 @@
|
|
|
8014
8039
|
const groupFrame = this.analysisFrameFromRays(group.rays, frame);
|
|
8015
8040
|
const groupDepthRange = this.depthRangeFromRays(group.rays, groupFrame);
|
|
8016
8041
|
const slices = this.collectSturmSlices(group.rays, groupFrame, groupDepthRange, DEFAULT_STURM_STEP_MM);
|
|
8017
|
-
const analysis = this.analyzeSturmSlices(slices, effectiveCylinderD);
|
|
8042
|
+
const analysis = this.analyzeSturmSlices(slices, effectiveCylinderD, profileWorldZBounds);
|
|
8018
8043
|
return {
|
|
8019
8044
|
line: group.line,
|
|
8020
8045
|
wavelength_nm: group.wavelength_nm,
|
|
@@ -8148,6 +8173,9 @@
|
|
|
8148
8173
|
const lambdaMinor = Math.max(0, trace / 2 - root);
|
|
8149
8174
|
const thetaRad = 0.5 * Math.atan2(2 * sxy, sxx - syy);
|
|
8150
8175
|
const angleMajorDeg = ((thetaRad * 180) / Math.PI + 360) % 180;
|
|
8176
|
+
const twoThetaRad = 2 * thetaRad;
|
|
8177
|
+
const j0 = Math.cos(twoThetaRad);
|
|
8178
|
+
const j45 = Math.sin(twoThetaRad);
|
|
8151
8179
|
const majorDirection = frame.u.clone().multiplyScalar(Math.cos(thetaRad))
|
|
8152
8180
|
.add(frame.v.clone().multiplyScalar(Math.sin(thetaRad)))
|
|
8153
8181
|
.normalize();
|
|
@@ -8157,6 +8185,8 @@
|
|
|
8157
8185
|
wMajor: Math.sqrt(lambdaMajor),
|
|
8158
8186
|
wMinor: Math.sqrt(lambdaMinor),
|
|
8159
8187
|
angleMajorDeg,
|
|
8188
|
+
j0,
|
|
8189
|
+
j45,
|
|
8160
8190
|
angleMinorDeg: (angleMajorDeg + 90) % 180,
|
|
8161
8191
|
majorDirection: {
|
|
8162
8192
|
x: majorDirection.x,
|
|
@@ -8194,6 +8224,84 @@
|
|
|
8194
8224
|
const d = Math.abs((((a - b) % 180) + 180) % 180);
|
|
8195
8225
|
return Math.min(d, 180 - d);
|
|
8196
8226
|
}
|
|
8227
|
+
phasorDot(p, q) {
|
|
8228
|
+
return p.j0 * q.j0 + p.j45 * q.j45;
|
|
8229
|
+
}
|
|
8230
|
+
/**
|
|
8231
|
+
* 평탄도 순으로 정렬된 슬라이스에서 선초점 쌍(직교 주경선)에 해당하는 Top2를 고른다.
|
|
8232
|
+
* 주경선 각 대신 이각 phasor (cos 2θ, sin 2θ)로 직교를 판정하고, 실패 시 각도·전역 검색·깊이 폴백을 사용한다.
|
|
8233
|
+
*/
|
|
8234
|
+
pickFlattestSturmPair(sortedByFlatness, top2MinGapMm, allSlices) {
|
|
8235
|
+
if (sortedByFlatness.length === 0)
|
|
8236
|
+
return [];
|
|
8237
|
+
const first = sortedByFlatness[0];
|
|
8238
|
+
const top2MinAngleGapDeg = DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG;
|
|
8239
|
+
const phasorStrict = DEFAULT_STURM_TOP2_PHASOR_OPPOSITION_MAX_DOT;
|
|
8240
|
+
const phasorLoose = DEFAULT_STURM_TOP2_PHASOR_FALLBACK_MAX_DOT;
|
|
8241
|
+
const lastResortMinDeg = DEFAULT_STURM_TOP2_AXIS_LAST_RESORT_MIN_GAP_DEG;
|
|
8242
|
+
const depthOk = (c) => Math.abs(c.depth - first.depth) >= top2MinGapMm;
|
|
8243
|
+
const byPhasor = (pool, maxDot) => pool.find((c) => (c !== first
|
|
8244
|
+
&& depthOk(c)
|
|
8245
|
+
&& this.phasorDot(first.profile, c.profile) <= maxDot));
|
|
8246
|
+
let second = byPhasor(sortedByFlatness, phasorStrict)
|
|
8247
|
+
?? byPhasor(sortedByFlatness, phasorLoose);
|
|
8248
|
+
if (!second) {
|
|
8249
|
+
second = sortedByFlatness.find((c) => (c !== first
|
|
8250
|
+
&& depthOk(c)
|
|
8251
|
+
&& this.axisDiffDeg(c.profile.angleMajorDeg, first.profile.angleMajorDeg) >= top2MinAngleGapDeg));
|
|
8252
|
+
}
|
|
8253
|
+
if (!second) {
|
|
8254
|
+
let best = null;
|
|
8255
|
+
let bestAxDiff = -1;
|
|
8256
|
+
for (const c of sortedByFlatness) {
|
|
8257
|
+
if (c === first || !depthOk(c))
|
|
8258
|
+
continue;
|
|
8259
|
+
const axd = this.axisDiffDeg(c.profile.angleMajorDeg, first.profile.angleMajorDeg);
|
|
8260
|
+
if (axd > bestAxDiff) {
|
|
8261
|
+
bestAxDiff = axd;
|
|
8262
|
+
best = c;
|
|
8263
|
+
}
|
|
8264
|
+
}
|
|
8265
|
+
if (best && bestAxDiff >= lastResortMinDeg)
|
|
8266
|
+
second = best;
|
|
8267
|
+
}
|
|
8268
|
+
// 평탄도 정렬 풀만으로는 같은 phasor만 연속으로 나오는 경우가 있어, 전 스캔에서 가장 반대인 phasor를 고른다.
|
|
8269
|
+
if (!second) {
|
|
8270
|
+
let best = null;
|
|
8271
|
+
let bestDot = 1;
|
|
8272
|
+
for (const c of allSlices) {
|
|
8273
|
+
if (c === first || !depthOk(c))
|
|
8274
|
+
continue;
|
|
8275
|
+
const d = this.phasorDot(first.profile, c.profile);
|
|
8276
|
+
if (d < bestDot) {
|
|
8277
|
+
bestDot = d;
|
|
8278
|
+
best = c;
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
if (best && bestDot < -0.08)
|
|
8282
|
+
second = best;
|
|
8283
|
+
}
|
|
8284
|
+
// 마지막: 가장 납작한 슬라이스와 깊이 차가 가장 큰 “비교적 납작한” 슬라이스 (선초점이 z로 분리될 때)
|
|
8285
|
+
if (!second) {
|
|
8286
|
+
const flatThreshold = first.ratio * 1.5 + 1e-6;
|
|
8287
|
+
let best = null;
|
|
8288
|
+
let bestDepthSpan = -1;
|
|
8289
|
+
for (const c of allSlices) {
|
|
8290
|
+
if (c === first || !depthOk(c))
|
|
8291
|
+
continue;
|
|
8292
|
+
if (c.ratio > flatThreshold)
|
|
8293
|
+
continue;
|
|
8294
|
+
const span = Math.abs(c.depth - first.depth);
|
|
8295
|
+
if (span > bestDepthSpan) {
|
|
8296
|
+
bestDepthSpan = span;
|
|
8297
|
+
best = c;
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
if (best)
|
|
8301
|
+
second = best;
|
|
8302
|
+
}
|
|
8303
|
+
return second ? [first, second] : [first];
|
|
8304
|
+
}
|
|
8197
8305
|
buildApproxCenter(flattestTop2, smallestEllipse, preferTop2Mid) {
|
|
8198
8306
|
if (flattestTop2.length <= 0)
|
|
8199
8307
|
return null;
|
|
@@ -8218,6 +8326,19 @@
|
|
|
8218
8326
|
const first = flattestTop2[0];
|
|
8219
8327
|
return { x: first.profile.at.x, y: first.profile.at.y, z: first.z, mode: "top1-flat" };
|
|
8220
8328
|
}
|
|
8329
|
+
slicesForSturmAnalysis(sturmSlices, bounds) {
|
|
8330
|
+
if (!bounds)
|
|
8331
|
+
return sturmSlices;
|
|
8332
|
+
const zMin = bounds.zMin;
|
|
8333
|
+
const zMax = bounds.zMax;
|
|
8334
|
+
if (!Number.isFinite(zMin) || !Number.isFinite(zMax) || zMax < zMin)
|
|
8335
|
+
return sturmSlices;
|
|
8336
|
+
const bounded = sturmSlices.filter((s) => {
|
|
8337
|
+
const z = s.profile?.at?.z;
|
|
8338
|
+
return Number.isFinite(z) && z >= zMin && z <= zMax;
|
|
8339
|
+
});
|
|
8340
|
+
return bounded.length >= 2 ? bounded : sturmSlices;
|
|
8341
|
+
}
|
|
8221
8342
|
groupByFraunhoferLine(rays) {
|
|
8222
8343
|
const groups = new Map();
|
|
8223
8344
|
for (const ray of rays) {
|
|
@@ -8238,21 +8359,17 @@
|
|
|
8238
8359
|
}
|
|
8239
8360
|
return [...groups.values()].sort((a, b) => this.lineOrder.indexOf(a.line) - this.lineOrder.indexOf(b.line));
|
|
8240
8361
|
}
|
|
8241
|
-
analyzeSturmSlices(sturmSlices, effectiveCylinderD) {
|
|
8362
|
+
analyzeSturmSlices(sturmSlices, effectiveCylinderD, profileWorldZBounds) {
|
|
8242
8363
|
const top2MinGapMm = DEFAULT_STURM_TOP2_MIN_GAP_MM;
|
|
8243
|
-
const top2MinAngleGapDeg = DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG;
|
|
8244
8364
|
const effectiveCylinderThresholdD = DEFAULT_EFFECTIVE_CYLINDER_THRESHOLD_D;
|
|
8245
8365
|
const preferTop2Mid = effectiveCylinderD >= effectiveCylinderThresholdD;
|
|
8246
|
-
const
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
&& this.axisDiffDeg(candidate.profile.angleMajorDeg, first.profile.angleMajorDeg) >= top2MinAngleGapDeg));
|
|
8252
|
-
flattestTop2 = second ? [first, second] : [first];
|
|
8253
|
-
}
|
|
8366
|
+
const slicesForAnalysis = this.slicesForSturmAnalysis(sturmSlices, profileWorldZBounds ?? null);
|
|
8367
|
+
const sortedByFlatness = [...slicesForAnalysis].sort((a, b) => a.ratio - b.ratio);
|
|
8368
|
+
const flattestTop2 = sortedByFlatness.length > 0
|
|
8369
|
+
? this.pickFlattestSturmPair(sortedByFlatness, top2MinGapMm, slicesForAnalysis)
|
|
8370
|
+
: [];
|
|
8254
8371
|
let smallestEllipse = null;
|
|
8255
|
-
for (const slice of
|
|
8372
|
+
for (const slice of slicesForAnalysis) {
|
|
8256
8373
|
if (!smallestEllipse || slice.size < smallestEllipse.size)
|
|
8257
8374
|
smallestEllipse = slice;
|
|
8258
8375
|
}
|
|
@@ -8876,8 +8993,10 @@
|
|
|
8876
8993
|
const eyeRotXDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.x);
|
|
8877
8994
|
const eyeRotYDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.y);
|
|
8878
8995
|
this.eyeTiltDeg = this.normalizeEyeTilt(eye?.tilt);
|
|
8879
|
-
|
|
8880
|
-
|
|
8996
|
+
// Apply eye deviation in the opposite direction so a lens with the same
|
|
8997
|
+
// prism/base prescription can optically compensate the eye deviation.
|
|
8998
|
+
const eyeEulerXDeg = eyeRotYDeg + this.eyeTiltDeg.x;
|
|
8999
|
+
const eyeEulerYDeg = (-eyeRotXDeg) + this.eyeTiltDeg.y;
|
|
8881
9000
|
this.eyeRotationQuaternion = new Quaternion().setFromEuler(new Euler((eyeEulerXDeg * Math.PI) / 180, (eyeEulerYDeg * Math.PI) / 180, 0, "XYZ"));
|
|
8882
9001
|
this.eyeRotationQuaternionInverse = this.eyeRotationQuaternion.clone().invert();
|
|
8883
9002
|
this.eyeRotationPivot = new Vector3(0, 0, SCAXEngineCore.EYE_ROTATION_PIVOT_FROM_CORNEA_MM);
|
|
@@ -9039,10 +9158,22 @@
|
|
|
9039
9158
|
* - x_deg/y_deg는 프리즘 회전량에 eye.tilt를 합산한 최종 렌더 회전량입니다.
|
|
9040
9159
|
*/
|
|
9041
9160
|
getEyeRotation() {
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9161
|
+
// Keep render rotation strictly aligned with the internal eye rotation used in ray tracing.
|
|
9162
|
+
// Internal eye-space Euler:
|
|
9163
|
+
// eyeEulerXDeg = eyeRotYDeg + tilt.x
|
|
9164
|
+
// eyeEulerYDeg = (-eyeRotXDeg) + tilt.y
|
|
9165
|
+
// UI mapping (applyEyeRenderRotation):
|
|
9166
|
+
// object.rotation.x = -y_deg
|
|
9167
|
+
// object.rotation.y = x_deg
|
|
9168
|
+
// so we expose:
|
|
9169
|
+
// x_deg = eyeEulerYDeg
|
|
9170
|
+
// y_deg = -eyeEulerXDeg
|
|
9171
|
+
const eyeRotXDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.x);
|
|
9172
|
+
const eyeRotYDeg = this.prismComponentToAngleDeg(this.eyePrismEffectVector.y);
|
|
9173
|
+
const eyeEulerXDeg = eyeRotYDeg + this.eyeTiltDeg.x;
|
|
9174
|
+
const eyeEulerYDeg = (-eyeRotXDeg) + this.eyeTiltDeg.y;
|
|
9175
|
+
const xDeg = eyeEulerYDeg;
|
|
9176
|
+
const yDeg = -eyeEulerXDeg;
|
|
9046
9177
|
return {
|
|
9047
9178
|
x_deg: xDeg,
|
|
9048
9179
|
y_deg: yDeg,
|
|
@@ -9102,12 +9233,40 @@
|
|
|
9102
9233
|
this.tracedRays = traced;
|
|
9103
9234
|
return traced;
|
|
9104
9235
|
}
|
|
9236
|
+
/**
|
|
9237
|
+
* Sturm 선초/근사중심에 사용할 슬라이스 centroid(world z) 구간:
|
|
9238
|
+
* 각막 전면(cornea_anterior) 이상 ~ 망막(retina) + 여유 이하.
|
|
9239
|
+
*/
|
|
9240
|
+
sturmEyeProfileWorldZBounds() {
|
|
9241
|
+
let corneaAnteriorZ = Number.POSITIVE_INFINITY;
|
|
9242
|
+
let retinaZ = Number.NEGATIVE_INFINITY;
|
|
9243
|
+
for (const surface of this.surfaces) {
|
|
9244
|
+
const name = String(surface.name || "").toLowerCase();
|
|
9245
|
+
const z = Number(this.readSurfacePosition(surface)?.z);
|
|
9246
|
+
if (!Number.isFinite(z))
|
|
9247
|
+
continue;
|
|
9248
|
+
if (name.includes("cornea") && name.includes("anterior")) {
|
|
9249
|
+
corneaAnteriorZ = Math.min(corneaAnteriorZ, z);
|
|
9250
|
+
}
|
|
9251
|
+
if (name === "retina") {
|
|
9252
|
+
retinaZ = Math.max(retinaZ, z);
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
if (!Number.isFinite(corneaAnteriorZ) || !Number.isFinite(retinaZ))
|
|
9256
|
+
return undefined;
|
|
9257
|
+
if (retinaZ + 1e-9 < corneaAnteriorZ)
|
|
9258
|
+
return undefined;
|
|
9259
|
+
return {
|
|
9260
|
+
zMin: corneaAnteriorZ,
|
|
9261
|
+
zMax: retinaZ + DEFAULT_STURM_PROFILE_WORLD_Z_PAST_RETINA_MM,
|
|
9262
|
+
};
|
|
9263
|
+
}
|
|
9105
9264
|
/**
|
|
9106
9265
|
* 2) Sturm calculation 전용 함수
|
|
9107
9266
|
* traced ray 집합에서 z-scan 기반 Sturm 슬라이스/근사 중심을 계산합니다.
|
|
9108
9267
|
*/
|
|
9109
9268
|
sturmCalculation(rays = this.tracedRays) {
|
|
9110
|
-
this.lastSturmGapAnalysis = this.sturm.calculate(rays, this.effectiveCylinderFromOpticSurfaces(), this.lastSourceRaysForSturm);
|
|
9269
|
+
this.lastSturmGapAnalysis = this.sturm.calculate(rays, this.effectiveCylinderFromOpticSurfaces(), this.lastSourceRaysForSturm, this.sturmEyeProfileWorldZBounds() ?? null);
|
|
9111
9270
|
return this.lastSturmGapAnalysis;
|
|
9112
9271
|
}
|
|
9113
9272
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as SCAXEngine } from "./scax-engine";
|
|
2
2
|
export { SCAXEngineCore } from "./scax-engine";
|
|
3
|
+
export type { SturmProfileWorldZBounds } from "./sturm/sturm";
|
|
3
4
|
export { default as Ray } from "./ray/ray";
|
|
4
5
|
export type { RayProps } from "./ray/ray";
|
|
5
6
|
export type { AffinePair } from "./affine/affine";
|
|
@@ -93,6 +93,12 @@ export declare const FRAUNHOFER_REFRACTIVE_INDICES: {
|
|
|
93
93
|
d: number;
|
|
94
94
|
C: number;
|
|
95
95
|
};
|
|
96
|
+
lens_navarro: {
|
|
97
|
+
F: number;
|
|
98
|
+
e: number;
|
|
99
|
+
d: number;
|
|
100
|
+
C: number;
|
|
101
|
+
};
|
|
96
102
|
lens_anterior: {
|
|
97
103
|
F: number;
|
|
98
104
|
e: number;
|
|
@@ -183,6 +189,19 @@ export declare const DEFAULT_STURM_TOP2_MIN_GAP_MM = 0;
|
|
|
183
189
|
* Sturm 분석에서 Top2 선택 시 허용하는 최소 축 각도 차(도)
|
|
184
190
|
*/
|
|
185
191
|
export declare const DEFAULT_STURM_TOP2_MIN_ANGLE_GAP_DEG = 45;
|
|
192
|
+
/**
|
|
193
|
+
* Sturm Top2 이각(J0,J45) phasor 내적 상한: cos(Δ₂θ) 이 이 값 이하일 때
|
|
194
|
+
* 두 주경선을 직교(선초점 쌍)로 취급한다. -1에 가까울수록 엄격.
|
|
195
|
+
*/
|
|
196
|
+
export declare const DEFAULT_STURM_TOP2_PHASOR_OPPOSITION_MAX_DOT = -0.86;
|
|
197
|
+
/**
|
|
198
|
+
* phasor 직교 판정 실패 시 완화 단계에서 쓰는 내적 상한
|
|
199
|
+
*/
|
|
200
|
+
export declare const DEFAULT_STURM_TOP2_PHASOR_FALLBACK_MAX_DOT = -0.55;
|
|
201
|
+
/**
|
|
202
|
+
* 축 각·phasor 폴백 후에도 후보가 없을 때 허용하는 최소 각도 차(도)
|
|
203
|
+
*/
|
|
204
|
+
export declare const DEFAULT_STURM_TOP2_AXIS_LAST_RESORT_MIN_GAP_DEG = 22;
|
|
186
205
|
/**
|
|
187
206
|
* 실효 난시량이 이 값(D) 이상이면 U/V 중간점을 CLC 근사 중심으로 우선 사용
|
|
188
207
|
*/
|
|
@@ -191,4 +210,9 @@ export declare const DEFAULT_EFFECTIVE_CYLINDER_THRESHOLD_D = 0.125;
|
|
|
191
210
|
* Sturm z-scan 간격(mm)
|
|
192
211
|
*/
|
|
193
212
|
export declare const DEFAULT_STURM_STEP_MM = 0.01;
|
|
213
|
+
/**
|
|
214
|
+
* Sturm 분석 슬라이스의 profile.at.z를 각막(앞)~망막+이 값(mm) 구간으로 한정할 때
|
|
215
|
+
* 망막 표면 z보다 진행 방향 +z로 허용하는 여유.
|
|
216
|
+
*/
|
|
217
|
+
export declare const DEFAULT_STURM_PROFILE_WORLD_Z_PAST_RETINA_MM = 4;
|
|
194
218
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RefractiveIndexSpec } from "../../optics/refractive-index";
|
|
1
2
|
import Surface from "../../surfaces/surface";
|
|
2
3
|
interface BaseEyeSurfaceParameter {
|
|
3
4
|
name: string;
|
|
@@ -6,15 +7,15 @@ interface BaseEyeSurfaceParameter {
|
|
|
6
7
|
interface SphericalEyeSurfaceParameter extends BaseEyeSurfaceParameter {
|
|
7
8
|
type: "spherical";
|
|
8
9
|
radius: number;
|
|
9
|
-
n_before:
|
|
10
|
-
n_after:
|
|
10
|
+
n_before: RefractiveIndexSpec;
|
|
11
|
+
n_after: RefractiveIndexSpec;
|
|
11
12
|
}
|
|
12
13
|
interface AsphericalEyeSurfaceParameter extends BaseEyeSurfaceParameter {
|
|
13
14
|
type: "aspherical";
|
|
14
15
|
radius: number;
|
|
15
16
|
conic: number;
|
|
16
|
-
n_before:
|
|
17
|
-
n_after:
|
|
17
|
+
n_before: RefractiveIndexSpec;
|
|
18
|
+
n_after: RefractiveIndexSpec;
|
|
18
19
|
}
|
|
19
20
|
interface SphericalImageEyeSurfaceParameter extends BaseEyeSurfaceParameter {
|
|
20
21
|
type: "spherical-image";
|
|
@@ -169,6 +169,11 @@ export declare class SCAXEngineCore {
|
|
|
169
169
|
* 광원에서 시작한 광선을 표면 순서대로 굴절시켜 최종 광선 집합을 반환합니다.
|
|
170
170
|
*/
|
|
171
171
|
rayTracing(): Ray[];
|
|
172
|
+
/**
|
|
173
|
+
* Sturm 선초/근사중심에 사용할 슬라이스 centroid(world z) 구간:
|
|
174
|
+
* 각막 전면(cornea_anterior) 이상 ~ 망막(retina) + 여유 이하.
|
|
175
|
+
*/
|
|
176
|
+
private sturmEyeProfileWorldZBounds;
|
|
172
177
|
/**
|
|
173
178
|
* 2) Sturm calculation 전용 함수
|
|
174
179
|
* traced ray 집합에서 z-scan 기반 Sturm 슬라이스/근사 중심을 계산합니다.
|
|
@@ -190,6 +195,8 @@ export declare class SCAXEngineCore {
|
|
|
190
195
|
wMajor: number;
|
|
191
196
|
wMinor: number;
|
|
192
197
|
angleMajorDeg: number;
|
|
198
|
+
j0: number;
|
|
199
|
+
j45: number;
|
|
193
200
|
angleMinorDeg: number;
|
|
194
201
|
majorDirection: {
|
|
195
202
|
x: number;
|
|
@@ -221,6 +228,8 @@ export declare class SCAXEngineCore {
|
|
|
221
228
|
wMajor: number;
|
|
222
229
|
wMinor: number;
|
|
223
230
|
angleMajorDeg: number;
|
|
231
|
+
j0: number;
|
|
232
|
+
j45: number;
|
|
224
233
|
angleMinorDeg: number;
|
|
225
234
|
majorDirection: {
|
|
226
235
|
x: number;
|
|
@@ -248,6 +257,8 @@ export declare class SCAXEngineCore {
|
|
|
248
257
|
wMajor: number;
|
|
249
258
|
wMinor: number;
|
|
250
259
|
angleMajorDeg: number;
|
|
260
|
+
j0: number;
|
|
261
|
+
j45: number;
|
|
251
262
|
angleMinorDeg: number;
|
|
252
263
|
majorDirection: {
|
|
253
264
|
x: number;
|
|
@@ -350,6 +361,8 @@ export default class SCAXEngine {
|
|
|
350
361
|
wMajor: number;
|
|
351
362
|
wMinor: number;
|
|
352
363
|
angleMajorDeg: number;
|
|
364
|
+
j0: number;
|
|
365
|
+
j45: number;
|
|
353
366
|
angleMinorDeg: number;
|
|
354
367
|
majorDirection: {
|
|
355
368
|
x: number;
|
|
@@ -381,6 +394,8 @@ export default class SCAXEngine {
|
|
|
381
394
|
wMajor: number;
|
|
382
395
|
wMinor: number;
|
|
383
396
|
angleMajorDeg: number;
|
|
397
|
+
j0: number;
|
|
398
|
+
j45: number;
|
|
384
399
|
angleMinorDeg: number;
|
|
385
400
|
majorDirection: {
|
|
386
401
|
x: number;
|
|
@@ -408,6 +423,8 @@ export default class SCAXEngine {
|
|
|
408
423
|
wMajor: number;
|
|
409
424
|
wMinor: number;
|
|
410
425
|
angleMajorDeg: number;
|
|
426
|
+
j0: number;
|
|
427
|
+
j45: number;
|
|
411
428
|
angleMinorDeg: number;
|
|
412
429
|
majorDirection: {
|
|
413
430
|
x: number;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import Ray from "../ray/ray";
|
|
2
|
+
/** Sturm 선초 분석에 사용할 슬라이스 centroid(world) z 구간 [zMin, zMax] (mm) */
|
|
3
|
+
export type SturmProfileWorldZBounds = {
|
|
4
|
+
zMin: number;
|
|
5
|
+
zMax: number;
|
|
6
|
+
};
|
|
2
7
|
type FraunhoferLine = "g" | "F" | "e" | "d" | "C" | "r";
|
|
3
8
|
type SturmSlice = {
|
|
4
9
|
z: number;
|
|
@@ -14,6 +19,9 @@ type SturmSlice = {
|
|
|
14
19
|
wMajor: number;
|
|
15
20
|
wMinor: number;
|
|
16
21
|
angleMajorDeg: number;
|
|
22
|
+
/** 이각 공간 난시 phasor: (cos 2θ, sin 2θ), θ = 주경선 각(rad) */
|
|
23
|
+
j0: number;
|
|
24
|
+
j45: number;
|
|
17
25
|
angleMinorDeg: number;
|
|
18
26
|
majorDirection: {
|
|
19
27
|
x: number;
|
|
@@ -34,7 +42,7 @@ type SturmSlice = {
|
|
|
34
42
|
*/
|
|
35
43
|
export default class Sturm {
|
|
36
44
|
private lastResult;
|
|
37
|
-
calculate(rays: Ray[], effectiveCylinderD: number, axisReferenceRays?: Ray[]): {
|
|
45
|
+
calculate(rays: Ray[], effectiveCylinderD: number, axisReferenceRays?: Ray[], profileWorldZBounds?: SturmProfileWorldZBounds | null): {
|
|
38
46
|
slices_info: {
|
|
39
47
|
count: number;
|
|
40
48
|
slices: SturmSlice[];
|
|
@@ -73,7 +81,14 @@ export default class Sturm {
|
|
|
73
81
|
private secondMomentProfileAtDepth;
|
|
74
82
|
private collectSturmSlices;
|
|
75
83
|
private axisDiffDeg;
|
|
84
|
+
private phasorDot;
|
|
85
|
+
/**
|
|
86
|
+
* 평탄도 순으로 정렬된 슬라이스에서 선초점 쌍(직교 주경선)에 해당하는 Top2를 고른다.
|
|
87
|
+
* 주경선 각 대신 이각 phasor (cos 2θ, sin 2θ)로 직교를 판정하고, 실패 시 각도·전역 검색·깊이 폴백을 사용한다.
|
|
88
|
+
*/
|
|
89
|
+
private pickFlattestSturmPair;
|
|
76
90
|
private buildApproxCenter;
|
|
91
|
+
private slicesForSturmAnalysis;
|
|
77
92
|
private groupByFraunhoferLine;
|
|
78
93
|
private analyzeSturmSlices;
|
|
79
94
|
}
|
package/package.json
CHANGED