scax-engine 0.2.0-beta.1 → 0.2.0-beta.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 +154 -152
- package/dist/scax-engine.js +153 -153
- package/dist/scax-engine.umd.js +154 -152
- package/dist/types/index.d.ts +3 -1
- package/dist/types/scax-engine.d.ts +5 -222
- package/package.json +1 -1
package/dist/scax-engine.cjs
CHANGED
|
@@ -7013,119 +7013,6 @@ class Ray {
|
|
|
7013
7013
|
}
|
|
7014
7014
|
}
|
|
7015
7015
|
|
|
7016
|
-
/**
|
|
7017
|
-
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7018
|
-
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7019
|
-
*/
|
|
7020
|
-
class Affine {
|
|
7021
|
-
constructor() {
|
|
7022
|
-
this.lastResult = null;
|
|
7023
|
-
this.lastPairs = [];
|
|
7024
|
-
}
|
|
7025
|
-
estimate(pairs) {
|
|
7026
|
-
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7027
|
-
this.lastPairs = inputPairs;
|
|
7028
|
-
const affine = this.fitAffine2D(inputPairs);
|
|
7029
|
-
if (!affine) {
|
|
7030
|
-
this.lastResult = null;
|
|
7031
|
-
return null;
|
|
7032
|
-
}
|
|
7033
|
-
let residualSumPct = 0;
|
|
7034
|
-
let residualCount = 0;
|
|
7035
|
-
let residualMaxPct = 0;
|
|
7036
|
-
const residuals = [];
|
|
7037
|
-
for (const pair of inputPairs) {
|
|
7038
|
-
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7039
|
-
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7040
|
-
const rx = pair.tx - px;
|
|
7041
|
-
const ry = pair.ty - py;
|
|
7042
|
-
const magnitude = Math.hypot(rx, ry);
|
|
7043
|
-
if (magnitude < 1e-4)
|
|
7044
|
-
continue;
|
|
7045
|
-
const radiusRef = Math.hypot(px, py);
|
|
7046
|
-
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7047
|
-
residualSumPct += pct;
|
|
7048
|
-
residualCount += 1;
|
|
7049
|
-
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7050
|
-
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7051
|
-
}
|
|
7052
|
-
const result = {
|
|
7053
|
-
...affine,
|
|
7054
|
-
count: inputPairs.length,
|
|
7055
|
-
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7056
|
-
residualMaxPct,
|
|
7057
|
-
residuals,
|
|
7058
|
-
};
|
|
7059
|
-
this.lastResult = result;
|
|
7060
|
-
return result;
|
|
7061
|
-
}
|
|
7062
|
-
/**
|
|
7063
|
-
* 마지막 affine 추정 결과를 반환합니다.
|
|
7064
|
-
*/
|
|
7065
|
-
getLastResult() {
|
|
7066
|
-
return this.lastResult;
|
|
7067
|
-
}
|
|
7068
|
-
/**
|
|
7069
|
-
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7070
|
-
*/
|
|
7071
|
-
getLastPairs() {
|
|
7072
|
-
return [...this.lastPairs];
|
|
7073
|
-
}
|
|
7074
|
-
fitAffine2D(pairs) {
|
|
7075
|
-
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7076
|
-
return null;
|
|
7077
|
-
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7078
|
-
const atb = Array(6).fill(0);
|
|
7079
|
-
const accumulate = (row, rhs) => {
|
|
7080
|
-
for (let i = 0; i < 6; i += 1) {
|
|
7081
|
-
atb[i] += row[i] * rhs;
|
|
7082
|
-
for (let j = 0; j < 6; j += 1)
|
|
7083
|
-
ata[i][j] += row[i] * row[j];
|
|
7084
|
-
}
|
|
7085
|
-
};
|
|
7086
|
-
for (const pair of pairs) {
|
|
7087
|
-
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7088
|
-
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7089
|
-
}
|
|
7090
|
-
const n = 6;
|
|
7091
|
-
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7092
|
-
for (let col = 0; col < n; col += 1) {
|
|
7093
|
-
let pivot = col;
|
|
7094
|
-
for (let row = col + 1; row < n; row += 1) {
|
|
7095
|
-
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7096
|
-
pivot = row;
|
|
7097
|
-
}
|
|
7098
|
-
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7099
|
-
return null;
|
|
7100
|
-
if (pivot !== col) {
|
|
7101
|
-
const temp = aug[col];
|
|
7102
|
-
aug[col] = aug[pivot];
|
|
7103
|
-
aug[pivot] = temp;
|
|
7104
|
-
}
|
|
7105
|
-
const divider = aug[col][col];
|
|
7106
|
-
for (let j = col; j <= n; j += 1)
|
|
7107
|
-
aug[col][j] /= divider;
|
|
7108
|
-
for (let row = 0; row < n; row += 1) {
|
|
7109
|
-
if (row === col)
|
|
7110
|
-
continue;
|
|
7111
|
-
const factor = aug[row][col];
|
|
7112
|
-
if (Math.abs(factor) < 1e-12)
|
|
7113
|
-
continue;
|
|
7114
|
-
for (let j = col; j <= n; j += 1)
|
|
7115
|
-
aug[row][j] -= factor * aug[col][j];
|
|
7116
|
-
}
|
|
7117
|
-
}
|
|
7118
|
-
return {
|
|
7119
|
-
a: aug[0][n],
|
|
7120
|
-
b: aug[1][n],
|
|
7121
|
-
c: aug[2][n],
|
|
7122
|
-
d: aug[3][n],
|
|
7123
|
-
e: aug[4][n],
|
|
7124
|
-
f: aug[5][n],
|
|
7125
|
-
};
|
|
7126
|
-
}
|
|
7127
|
-
}
|
|
7128
|
-
|
|
7129
7016
|
class LightSource {
|
|
7130
7017
|
constructor() {
|
|
7131
7018
|
this.rays = [];
|
|
@@ -7337,6 +7224,156 @@ class RadialLightSource extends LightSource {
|
|
|
7337
7224
|
}
|
|
7338
7225
|
}
|
|
7339
7226
|
|
|
7227
|
+
class Surface {
|
|
7228
|
+
constructor(props) {
|
|
7229
|
+
this.type = "";
|
|
7230
|
+
this.name = "";
|
|
7231
|
+
this.position = new Vector3(0, 0, 0);
|
|
7232
|
+
this.tilt = new Vector2(0, 0);
|
|
7233
|
+
this.meridians = [];
|
|
7234
|
+
const { type, name, position, tilt } = props;
|
|
7235
|
+
this.type = type;
|
|
7236
|
+
this.name = name;
|
|
7237
|
+
this.position = new Vector3(position.x, position.y, position.z);
|
|
7238
|
+
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7239
|
+
this.incidentRays = [];
|
|
7240
|
+
this.refractedRays = [];
|
|
7241
|
+
}
|
|
7242
|
+
clearTraceHistory() {
|
|
7243
|
+
this.incidentRays = [];
|
|
7244
|
+
this.refractedRays = [];
|
|
7245
|
+
}
|
|
7246
|
+
getWorldPosition() {
|
|
7247
|
+
return this.position.clone();
|
|
7248
|
+
}
|
|
7249
|
+
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7250
|
+
this.position.copy(position);
|
|
7251
|
+
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7252
|
+
}
|
|
7253
|
+
/**
|
|
7254
|
+
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7255
|
+
*/
|
|
7256
|
+
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7257
|
+
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7258
|
+
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7259
|
+
this.position.copy(p1);
|
|
7260
|
+
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
|
|
7264
|
+
/**
|
|
7265
|
+
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7266
|
+
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7267
|
+
*/
|
|
7268
|
+
class Affine {
|
|
7269
|
+
constructor() {
|
|
7270
|
+
this.lastResult = null;
|
|
7271
|
+
this.lastPairs = [];
|
|
7272
|
+
}
|
|
7273
|
+
estimate(pairs) {
|
|
7274
|
+
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7275
|
+
this.lastPairs = inputPairs;
|
|
7276
|
+
const affine = this.fitAffine2D(inputPairs);
|
|
7277
|
+
if (!affine) {
|
|
7278
|
+
this.lastResult = null;
|
|
7279
|
+
return null;
|
|
7280
|
+
}
|
|
7281
|
+
let residualSumPct = 0;
|
|
7282
|
+
let residualCount = 0;
|
|
7283
|
+
let residualMaxPct = 0;
|
|
7284
|
+
const residuals = [];
|
|
7285
|
+
for (const pair of inputPairs) {
|
|
7286
|
+
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7287
|
+
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7288
|
+
const rx = pair.tx - px;
|
|
7289
|
+
const ry = pair.ty - py;
|
|
7290
|
+
const magnitude = Math.hypot(rx, ry);
|
|
7291
|
+
if (magnitude < 1e-4)
|
|
7292
|
+
continue;
|
|
7293
|
+
const radiusRef = Math.hypot(px, py);
|
|
7294
|
+
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7295
|
+
residualSumPct += pct;
|
|
7296
|
+
residualCount += 1;
|
|
7297
|
+
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7298
|
+
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7299
|
+
}
|
|
7300
|
+
const result = {
|
|
7301
|
+
...affine,
|
|
7302
|
+
count: inputPairs.length,
|
|
7303
|
+
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7304
|
+
residualMaxPct,
|
|
7305
|
+
residuals,
|
|
7306
|
+
};
|
|
7307
|
+
this.lastResult = result;
|
|
7308
|
+
return result;
|
|
7309
|
+
}
|
|
7310
|
+
/**
|
|
7311
|
+
* 마지막 affine 추정 결과를 반환합니다.
|
|
7312
|
+
*/
|
|
7313
|
+
getLastResult() {
|
|
7314
|
+
return this.lastResult;
|
|
7315
|
+
}
|
|
7316
|
+
/**
|
|
7317
|
+
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7318
|
+
*/
|
|
7319
|
+
getLastPairs() {
|
|
7320
|
+
return [...this.lastPairs];
|
|
7321
|
+
}
|
|
7322
|
+
fitAffine2D(pairs) {
|
|
7323
|
+
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7324
|
+
return null;
|
|
7325
|
+
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7326
|
+
const atb = Array(6).fill(0);
|
|
7327
|
+
const accumulate = (row, rhs) => {
|
|
7328
|
+
for (let i = 0; i < 6; i += 1) {
|
|
7329
|
+
atb[i] += row[i] * rhs;
|
|
7330
|
+
for (let j = 0; j < 6; j += 1)
|
|
7331
|
+
ata[i][j] += row[i] * row[j];
|
|
7332
|
+
}
|
|
7333
|
+
};
|
|
7334
|
+
for (const pair of pairs) {
|
|
7335
|
+
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7336
|
+
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7337
|
+
}
|
|
7338
|
+
const n = 6;
|
|
7339
|
+
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7340
|
+
for (let col = 0; col < n; col += 1) {
|
|
7341
|
+
let pivot = col;
|
|
7342
|
+
for (let row = col + 1; row < n; row += 1) {
|
|
7343
|
+
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7344
|
+
pivot = row;
|
|
7345
|
+
}
|
|
7346
|
+
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7347
|
+
return null;
|
|
7348
|
+
if (pivot !== col) {
|
|
7349
|
+
const temp = aug[col];
|
|
7350
|
+
aug[col] = aug[pivot];
|
|
7351
|
+
aug[pivot] = temp;
|
|
7352
|
+
}
|
|
7353
|
+
const divider = aug[col][col];
|
|
7354
|
+
for (let j = col; j <= n; j += 1)
|
|
7355
|
+
aug[col][j] /= divider;
|
|
7356
|
+
for (let row = 0; row < n; row += 1) {
|
|
7357
|
+
if (row === col)
|
|
7358
|
+
continue;
|
|
7359
|
+
const factor = aug[row][col];
|
|
7360
|
+
if (Math.abs(factor) < 1e-12)
|
|
7361
|
+
continue;
|
|
7362
|
+
for (let j = col; j <= n; j += 1)
|
|
7363
|
+
aug[row][j] -= factor * aug[col][j];
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
return {
|
|
7367
|
+
a: aug[0][n],
|
|
7368
|
+
b: aug[1][n],
|
|
7369
|
+
c: aug[2][n],
|
|
7370
|
+
d: aug[3][n],
|
|
7371
|
+
e: aug[4][n],
|
|
7372
|
+
f: aug[5][n],
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7340
7377
|
/** 각막 기준 안구 회전점 z(mm). `SCAXEngineCore`와 동일. */
|
|
7341
7378
|
const EYE_ROTATION_PIVOT_FROM_CORNEA_MM = 13;
|
|
7342
7379
|
function normalizePrismAmount$2(value) {
|
|
@@ -7409,43 +7446,6 @@ function applyRigidEyePoseToSurfaces(surfaces, pivot, rotation) {
|
|
|
7409
7446
|
}
|
|
7410
7447
|
}
|
|
7411
7448
|
|
|
7412
|
-
class Surface {
|
|
7413
|
-
constructor(props) {
|
|
7414
|
-
this.type = "";
|
|
7415
|
-
this.name = "";
|
|
7416
|
-
this.position = new Vector3(0, 0, 0);
|
|
7417
|
-
this.tilt = new Vector2(0, 0);
|
|
7418
|
-
this.meridians = [];
|
|
7419
|
-
const { type, name, position, tilt } = props;
|
|
7420
|
-
this.type = type;
|
|
7421
|
-
this.name = name;
|
|
7422
|
-
this.position = new Vector3(position.x, position.y, position.z);
|
|
7423
|
-
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7424
|
-
this.incidentRays = [];
|
|
7425
|
-
this.refractedRays = [];
|
|
7426
|
-
}
|
|
7427
|
-
clearTraceHistory() {
|
|
7428
|
-
this.incidentRays = [];
|
|
7429
|
-
this.refractedRays = [];
|
|
7430
|
-
}
|
|
7431
|
-
getWorldPosition() {
|
|
7432
|
-
return this.position.clone();
|
|
7433
|
-
}
|
|
7434
|
-
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7435
|
-
this.position.copy(position);
|
|
7436
|
-
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7437
|
-
}
|
|
7438
|
-
/**
|
|
7439
|
-
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7440
|
-
*/
|
|
7441
|
-
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7442
|
-
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7443
|
-
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7444
|
-
this.position.copy(p1);
|
|
7445
|
-
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7446
|
-
}
|
|
7447
|
-
}
|
|
7448
|
-
|
|
7449
7449
|
class ApertureStopSurface extends Surface {
|
|
7450
7450
|
constructor(props) {
|
|
7451
7451
|
super({
|
|
@@ -9625,8 +9625,8 @@ class SCAXEngine {
|
|
|
9625
9625
|
const orthogonalTabo = (taboAxis + 90) % 180;
|
|
9626
9626
|
const r = Math.hypot(j0, j45);
|
|
9627
9627
|
return [
|
|
9628
|
-
{ tabo: taboAxis, d: m
|
|
9629
|
-
{ tabo: orthogonalTabo, d: m
|
|
9628
|
+
{ tabo: taboAxis, d: m + r },
|
|
9629
|
+
{ tabo: orthogonalTabo, d: m - r },
|
|
9630
9630
|
].sort((a, b) => a.d - b.d);
|
|
9631
9631
|
}
|
|
9632
9632
|
calculateEyeRotationByPrism(prism) {
|
|
@@ -9641,7 +9641,9 @@ class SCAXEngine {
|
|
|
9641
9641
|
}
|
|
9642
9642
|
}
|
|
9643
9643
|
|
|
9644
|
+
exports.LightSource = LightSource;
|
|
9644
9645
|
exports.Ray = Ray;
|
|
9645
9646
|
exports.SCAXEngine = SCAXEngine;
|
|
9646
9647
|
exports.SCAXEngineCore = SCAXEngineCore;
|
|
9648
|
+
exports.Surface = Surface;
|
|
9647
9649
|
//# sourceMappingURL=scax-engine.cjs.map
|
package/dist/scax-engine.js
CHANGED
|
@@ -7011,119 +7011,6 @@ class Ray {
|
|
|
7011
7011
|
}
|
|
7012
7012
|
}
|
|
7013
7013
|
|
|
7014
|
-
/**
|
|
7015
|
-
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7016
|
-
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7017
|
-
*/
|
|
7018
|
-
class Affine {
|
|
7019
|
-
constructor() {
|
|
7020
|
-
this.lastResult = null;
|
|
7021
|
-
this.lastPairs = [];
|
|
7022
|
-
}
|
|
7023
|
-
estimate(pairs) {
|
|
7024
|
-
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7025
|
-
this.lastPairs = inputPairs;
|
|
7026
|
-
const affine = this.fitAffine2D(inputPairs);
|
|
7027
|
-
if (!affine) {
|
|
7028
|
-
this.lastResult = null;
|
|
7029
|
-
return null;
|
|
7030
|
-
}
|
|
7031
|
-
let residualSumPct = 0;
|
|
7032
|
-
let residualCount = 0;
|
|
7033
|
-
let residualMaxPct = 0;
|
|
7034
|
-
const residuals = [];
|
|
7035
|
-
for (const pair of inputPairs) {
|
|
7036
|
-
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7037
|
-
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7038
|
-
const rx = pair.tx - px;
|
|
7039
|
-
const ry = pair.ty - py;
|
|
7040
|
-
const magnitude = Math.hypot(rx, ry);
|
|
7041
|
-
if (magnitude < 1e-4)
|
|
7042
|
-
continue;
|
|
7043
|
-
const radiusRef = Math.hypot(px, py);
|
|
7044
|
-
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7045
|
-
residualSumPct += pct;
|
|
7046
|
-
residualCount += 1;
|
|
7047
|
-
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7048
|
-
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7049
|
-
}
|
|
7050
|
-
const result = {
|
|
7051
|
-
...affine,
|
|
7052
|
-
count: inputPairs.length,
|
|
7053
|
-
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7054
|
-
residualMaxPct,
|
|
7055
|
-
residuals,
|
|
7056
|
-
};
|
|
7057
|
-
this.lastResult = result;
|
|
7058
|
-
return result;
|
|
7059
|
-
}
|
|
7060
|
-
/**
|
|
7061
|
-
* 마지막 affine 추정 결과를 반환합니다.
|
|
7062
|
-
*/
|
|
7063
|
-
getLastResult() {
|
|
7064
|
-
return this.lastResult;
|
|
7065
|
-
}
|
|
7066
|
-
/**
|
|
7067
|
-
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7068
|
-
*/
|
|
7069
|
-
getLastPairs() {
|
|
7070
|
-
return [...this.lastPairs];
|
|
7071
|
-
}
|
|
7072
|
-
fitAffine2D(pairs) {
|
|
7073
|
-
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7074
|
-
return null;
|
|
7075
|
-
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7076
|
-
const atb = Array(6).fill(0);
|
|
7077
|
-
const accumulate = (row, rhs) => {
|
|
7078
|
-
for (let i = 0; i < 6; i += 1) {
|
|
7079
|
-
atb[i] += row[i] * rhs;
|
|
7080
|
-
for (let j = 0; j < 6; j += 1)
|
|
7081
|
-
ata[i][j] += row[i] * row[j];
|
|
7082
|
-
}
|
|
7083
|
-
};
|
|
7084
|
-
for (const pair of pairs) {
|
|
7085
|
-
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7086
|
-
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7087
|
-
}
|
|
7088
|
-
const n = 6;
|
|
7089
|
-
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7090
|
-
for (let col = 0; col < n; col += 1) {
|
|
7091
|
-
let pivot = col;
|
|
7092
|
-
for (let row = col + 1; row < n; row += 1) {
|
|
7093
|
-
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7094
|
-
pivot = row;
|
|
7095
|
-
}
|
|
7096
|
-
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7097
|
-
return null;
|
|
7098
|
-
if (pivot !== col) {
|
|
7099
|
-
const temp = aug[col];
|
|
7100
|
-
aug[col] = aug[pivot];
|
|
7101
|
-
aug[pivot] = temp;
|
|
7102
|
-
}
|
|
7103
|
-
const divider = aug[col][col];
|
|
7104
|
-
for (let j = col; j <= n; j += 1)
|
|
7105
|
-
aug[col][j] /= divider;
|
|
7106
|
-
for (let row = 0; row < n; row += 1) {
|
|
7107
|
-
if (row === col)
|
|
7108
|
-
continue;
|
|
7109
|
-
const factor = aug[row][col];
|
|
7110
|
-
if (Math.abs(factor) < 1e-12)
|
|
7111
|
-
continue;
|
|
7112
|
-
for (let j = col; j <= n; j += 1)
|
|
7113
|
-
aug[row][j] -= factor * aug[col][j];
|
|
7114
|
-
}
|
|
7115
|
-
}
|
|
7116
|
-
return {
|
|
7117
|
-
a: aug[0][n],
|
|
7118
|
-
b: aug[1][n],
|
|
7119
|
-
c: aug[2][n],
|
|
7120
|
-
d: aug[3][n],
|
|
7121
|
-
e: aug[4][n],
|
|
7122
|
-
f: aug[5][n],
|
|
7123
|
-
};
|
|
7124
|
-
}
|
|
7125
|
-
}
|
|
7126
|
-
|
|
7127
7014
|
class LightSource {
|
|
7128
7015
|
constructor() {
|
|
7129
7016
|
this.rays = [];
|
|
@@ -7335,6 +7222,156 @@ class RadialLightSource extends LightSource {
|
|
|
7335
7222
|
}
|
|
7336
7223
|
}
|
|
7337
7224
|
|
|
7225
|
+
class Surface {
|
|
7226
|
+
constructor(props) {
|
|
7227
|
+
this.type = "";
|
|
7228
|
+
this.name = "";
|
|
7229
|
+
this.position = new Vector3(0, 0, 0);
|
|
7230
|
+
this.tilt = new Vector2(0, 0);
|
|
7231
|
+
this.meridians = [];
|
|
7232
|
+
const { type, name, position, tilt } = props;
|
|
7233
|
+
this.type = type;
|
|
7234
|
+
this.name = name;
|
|
7235
|
+
this.position = new Vector3(position.x, position.y, position.z);
|
|
7236
|
+
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7237
|
+
this.incidentRays = [];
|
|
7238
|
+
this.refractedRays = [];
|
|
7239
|
+
}
|
|
7240
|
+
clearTraceHistory() {
|
|
7241
|
+
this.incidentRays = [];
|
|
7242
|
+
this.refractedRays = [];
|
|
7243
|
+
}
|
|
7244
|
+
getWorldPosition() {
|
|
7245
|
+
return this.position.clone();
|
|
7246
|
+
}
|
|
7247
|
+
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7248
|
+
this.position.copy(position);
|
|
7249
|
+
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7250
|
+
}
|
|
7251
|
+
/**
|
|
7252
|
+
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7253
|
+
*/
|
|
7254
|
+
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7255
|
+
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7256
|
+
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7257
|
+
this.position.copy(p1);
|
|
7258
|
+
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
/**
|
|
7263
|
+
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7264
|
+
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7265
|
+
*/
|
|
7266
|
+
class Affine {
|
|
7267
|
+
constructor() {
|
|
7268
|
+
this.lastResult = null;
|
|
7269
|
+
this.lastPairs = [];
|
|
7270
|
+
}
|
|
7271
|
+
estimate(pairs) {
|
|
7272
|
+
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7273
|
+
this.lastPairs = inputPairs;
|
|
7274
|
+
const affine = this.fitAffine2D(inputPairs);
|
|
7275
|
+
if (!affine) {
|
|
7276
|
+
this.lastResult = null;
|
|
7277
|
+
return null;
|
|
7278
|
+
}
|
|
7279
|
+
let residualSumPct = 0;
|
|
7280
|
+
let residualCount = 0;
|
|
7281
|
+
let residualMaxPct = 0;
|
|
7282
|
+
const residuals = [];
|
|
7283
|
+
for (const pair of inputPairs) {
|
|
7284
|
+
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7285
|
+
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7286
|
+
const rx = pair.tx - px;
|
|
7287
|
+
const ry = pair.ty - py;
|
|
7288
|
+
const magnitude = Math.hypot(rx, ry);
|
|
7289
|
+
if (magnitude < 1e-4)
|
|
7290
|
+
continue;
|
|
7291
|
+
const radiusRef = Math.hypot(px, py);
|
|
7292
|
+
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7293
|
+
residualSumPct += pct;
|
|
7294
|
+
residualCount += 1;
|
|
7295
|
+
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7296
|
+
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7297
|
+
}
|
|
7298
|
+
const result = {
|
|
7299
|
+
...affine,
|
|
7300
|
+
count: inputPairs.length,
|
|
7301
|
+
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7302
|
+
residualMaxPct,
|
|
7303
|
+
residuals,
|
|
7304
|
+
};
|
|
7305
|
+
this.lastResult = result;
|
|
7306
|
+
return result;
|
|
7307
|
+
}
|
|
7308
|
+
/**
|
|
7309
|
+
* 마지막 affine 추정 결과를 반환합니다.
|
|
7310
|
+
*/
|
|
7311
|
+
getLastResult() {
|
|
7312
|
+
return this.lastResult;
|
|
7313
|
+
}
|
|
7314
|
+
/**
|
|
7315
|
+
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7316
|
+
*/
|
|
7317
|
+
getLastPairs() {
|
|
7318
|
+
return [...this.lastPairs];
|
|
7319
|
+
}
|
|
7320
|
+
fitAffine2D(pairs) {
|
|
7321
|
+
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7322
|
+
return null;
|
|
7323
|
+
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7324
|
+
const atb = Array(6).fill(0);
|
|
7325
|
+
const accumulate = (row, rhs) => {
|
|
7326
|
+
for (let i = 0; i < 6; i += 1) {
|
|
7327
|
+
atb[i] += row[i] * rhs;
|
|
7328
|
+
for (let j = 0; j < 6; j += 1)
|
|
7329
|
+
ata[i][j] += row[i] * row[j];
|
|
7330
|
+
}
|
|
7331
|
+
};
|
|
7332
|
+
for (const pair of pairs) {
|
|
7333
|
+
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7334
|
+
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7335
|
+
}
|
|
7336
|
+
const n = 6;
|
|
7337
|
+
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7338
|
+
for (let col = 0; col < n; col += 1) {
|
|
7339
|
+
let pivot = col;
|
|
7340
|
+
for (let row = col + 1; row < n; row += 1) {
|
|
7341
|
+
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7342
|
+
pivot = row;
|
|
7343
|
+
}
|
|
7344
|
+
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7345
|
+
return null;
|
|
7346
|
+
if (pivot !== col) {
|
|
7347
|
+
const temp = aug[col];
|
|
7348
|
+
aug[col] = aug[pivot];
|
|
7349
|
+
aug[pivot] = temp;
|
|
7350
|
+
}
|
|
7351
|
+
const divider = aug[col][col];
|
|
7352
|
+
for (let j = col; j <= n; j += 1)
|
|
7353
|
+
aug[col][j] /= divider;
|
|
7354
|
+
for (let row = 0; row < n; row += 1) {
|
|
7355
|
+
if (row === col)
|
|
7356
|
+
continue;
|
|
7357
|
+
const factor = aug[row][col];
|
|
7358
|
+
if (Math.abs(factor) < 1e-12)
|
|
7359
|
+
continue;
|
|
7360
|
+
for (let j = col; j <= n; j += 1)
|
|
7361
|
+
aug[row][j] -= factor * aug[col][j];
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
return {
|
|
7365
|
+
a: aug[0][n],
|
|
7366
|
+
b: aug[1][n],
|
|
7367
|
+
c: aug[2][n],
|
|
7368
|
+
d: aug[3][n],
|
|
7369
|
+
e: aug[4][n],
|
|
7370
|
+
f: aug[5][n],
|
|
7371
|
+
};
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7338
7375
|
/** 각막 기준 안구 회전점 z(mm). `SCAXEngineCore`와 동일. */
|
|
7339
7376
|
const EYE_ROTATION_PIVOT_FROM_CORNEA_MM = 13;
|
|
7340
7377
|
function normalizePrismAmount$2(value) {
|
|
@@ -7407,43 +7444,6 @@ function applyRigidEyePoseToSurfaces(surfaces, pivot, rotation) {
|
|
|
7407
7444
|
}
|
|
7408
7445
|
}
|
|
7409
7446
|
|
|
7410
|
-
class Surface {
|
|
7411
|
-
constructor(props) {
|
|
7412
|
-
this.type = "";
|
|
7413
|
-
this.name = "";
|
|
7414
|
-
this.position = new Vector3(0, 0, 0);
|
|
7415
|
-
this.tilt = new Vector2(0, 0);
|
|
7416
|
-
this.meridians = [];
|
|
7417
|
-
const { type, name, position, tilt } = props;
|
|
7418
|
-
this.type = type;
|
|
7419
|
-
this.name = name;
|
|
7420
|
-
this.position = new Vector3(position.x, position.y, position.z);
|
|
7421
|
-
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7422
|
-
this.incidentRays = [];
|
|
7423
|
-
this.refractedRays = [];
|
|
7424
|
-
}
|
|
7425
|
-
clearTraceHistory() {
|
|
7426
|
-
this.incidentRays = [];
|
|
7427
|
-
this.refractedRays = [];
|
|
7428
|
-
}
|
|
7429
|
-
getWorldPosition() {
|
|
7430
|
-
return this.position.clone();
|
|
7431
|
-
}
|
|
7432
|
-
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7433
|
-
this.position.copy(position);
|
|
7434
|
-
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7435
|
-
}
|
|
7436
|
-
/**
|
|
7437
|
-
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7438
|
-
*/
|
|
7439
|
-
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7440
|
-
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7441
|
-
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7442
|
-
this.position.copy(p1);
|
|
7443
|
-
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7444
|
-
}
|
|
7445
|
-
}
|
|
7446
|
-
|
|
7447
7447
|
class ApertureStopSurface extends Surface {
|
|
7448
7448
|
constructor(props) {
|
|
7449
7449
|
super({
|
|
@@ -9623,8 +9623,8 @@ class SCAXEngine {
|
|
|
9623
9623
|
const orthogonalTabo = (taboAxis + 90) % 180;
|
|
9624
9624
|
const r = Math.hypot(j0, j45);
|
|
9625
9625
|
return [
|
|
9626
|
-
{ tabo: taboAxis, d: m
|
|
9627
|
-
{ tabo: orthogonalTabo, d: m
|
|
9626
|
+
{ tabo: taboAxis, d: m + r },
|
|
9627
|
+
{ tabo: orthogonalTabo, d: m - r },
|
|
9628
9628
|
].sort((a, b) => a.d - b.d);
|
|
9629
9629
|
}
|
|
9630
9630
|
calculateEyeRotationByPrism(prism) {
|
|
@@ -9639,5 +9639,5 @@ class SCAXEngine {
|
|
|
9639
9639
|
}
|
|
9640
9640
|
}
|
|
9641
9641
|
|
|
9642
|
-
export { Ray, SCAXEngine, SCAXEngineCore };
|
|
9642
|
+
export { LightSource, Ray, SCAXEngine, SCAXEngineCore, Surface };
|
|
9643
9643
|
//# sourceMappingURL=scax-engine.js.map
|
package/dist/scax-engine.umd.js
CHANGED
|
@@ -7017,119 +7017,6 @@
|
|
|
7017
7017
|
}
|
|
7018
7018
|
}
|
|
7019
7019
|
|
|
7020
|
-
/**
|
|
7021
|
-
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7022
|
-
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7023
|
-
*/
|
|
7024
|
-
class Affine {
|
|
7025
|
-
constructor() {
|
|
7026
|
-
this.lastResult = null;
|
|
7027
|
-
this.lastPairs = [];
|
|
7028
|
-
}
|
|
7029
|
-
estimate(pairs) {
|
|
7030
|
-
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7031
|
-
this.lastPairs = inputPairs;
|
|
7032
|
-
const affine = this.fitAffine2D(inputPairs);
|
|
7033
|
-
if (!affine) {
|
|
7034
|
-
this.lastResult = null;
|
|
7035
|
-
return null;
|
|
7036
|
-
}
|
|
7037
|
-
let residualSumPct = 0;
|
|
7038
|
-
let residualCount = 0;
|
|
7039
|
-
let residualMaxPct = 0;
|
|
7040
|
-
const residuals = [];
|
|
7041
|
-
for (const pair of inputPairs) {
|
|
7042
|
-
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7043
|
-
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7044
|
-
const rx = pair.tx - px;
|
|
7045
|
-
const ry = pair.ty - py;
|
|
7046
|
-
const magnitude = Math.hypot(rx, ry);
|
|
7047
|
-
if (magnitude < 1e-4)
|
|
7048
|
-
continue;
|
|
7049
|
-
const radiusRef = Math.hypot(px, py);
|
|
7050
|
-
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7051
|
-
residualSumPct += pct;
|
|
7052
|
-
residualCount += 1;
|
|
7053
|
-
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7054
|
-
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7055
|
-
}
|
|
7056
|
-
const result = {
|
|
7057
|
-
...affine,
|
|
7058
|
-
count: inputPairs.length,
|
|
7059
|
-
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7060
|
-
residualMaxPct,
|
|
7061
|
-
residuals,
|
|
7062
|
-
};
|
|
7063
|
-
this.lastResult = result;
|
|
7064
|
-
return result;
|
|
7065
|
-
}
|
|
7066
|
-
/**
|
|
7067
|
-
* 마지막 affine 추정 결과를 반환합니다.
|
|
7068
|
-
*/
|
|
7069
|
-
getLastResult() {
|
|
7070
|
-
return this.lastResult;
|
|
7071
|
-
}
|
|
7072
|
-
/**
|
|
7073
|
-
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7074
|
-
*/
|
|
7075
|
-
getLastPairs() {
|
|
7076
|
-
return [...this.lastPairs];
|
|
7077
|
-
}
|
|
7078
|
-
fitAffine2D(pairs) {
|
|
7079
|
-
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7080
|
-
return null;
|
|
7081
|
-
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7082
|
-
const atb = Array(6).fill(0);
|
|
7083
|
-
const accumulate = (row, rhs) => {
|
|
7084
|
-
for (let i = 0; i < 6; i += 1) {
|
|
7085
|
-
atb[i] += row[i] * rhs;
|
|
7086
|
-
for (let j = 0; j < 6; j += 1)
|
|
7087
|
-
ata[i][j] += row[i] * row[j];
|
|
7088
|
-
}
|
|
7089
|
-
};
|
|
7090
|
-
for (const pair of pairs) {
|
|
7091
|
-
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7092
|
-
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7093
|
-
}
|
|
7094
|
-
const n = 6;
|
|
7095
|
-
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7096
|
-
for (let col = 0; col < n; col += 1) {
|
|
7097
|
-
let pivot = col;
|
|
7098
|
-
for (let row = col + 1; row < n; row += 1) {
|
|
7099
|
-
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7100
|
-
pivot = row;
|
|
7101
|
-
}
|
|
7102
|
-
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7103
|
-
return null;
|
|
7104
|
-
if (pivot !== col) {
|
|
7105
|
-
const temp = aug[col];
|
|
7106
|
-
aug[col] = aug[pivot];
|
|
7107
|
-
aug[pivot] = temp;
|
|
7108
|
-
}
|
|
7109
|
-
const divider = aug[col][col];
|
|
7110
|
-
for (let j = col; j <= n; j += 1)
|
|
7111
|
-
aug[col][j] /= divider;
|
|
7112
|
-
for (let row = 0; row < n; row += 1) {
|
|
7113
|
-
if (row === col)
|
|
7114
|
-
continue;
|
|
7115
|
-
const factor = aug[row][col];
|
|
7116
|
-
if (Math.abs(factor) < 1e-12)
|
|
7117
|
-
continue;
|
|
7118
|
-
for (let j = col; j <= n; j += 1)
|
|
7119
|
-
aug[row][j] -= factor * aug[col][j];
|
|
7120
|
-
}
|
|
7121
|
-
}
|
|
7122
|
-
return {
|
|
7123
|
-
a: aug[0][n],
|
|
7124
|
-
b: aug[1][n],
|
|
7125
|
-
c: aug[2][n],
|
|
7126
|
-
d: aug[3][n],
|
|
7127
|
-
e: aug[4][n],
|
|
7128
|
-
f: aug[5][n],
|
|
7129
|
-
};
|
|
7130
|
-
}
|
|
7131
|
-
}
|
|
7132
|
-
|
|
7133
7020
|
class LightSource {
|
|
7134
7021
|
constructor() {
|
|
7135
7022
|
this.rays = [];
|
|
@@ -7341,6 +7228,156 @@
|
|
|
7341
7228
|
}
|
|
7342
7229
|
}
|
|
7343
7230
|
|
|
7231
|
+
class Surface {
|
|
7232
|
+
constructor(props) {
|
|
7233
|
+
this.type = "";
|
|
7234
|
+
this.name = "";
|
|
7235
|
+
this.position = new Vector3(0, 0, 0);
|
|
7236
|
+
this.tilt = new Vector2(0, 0);
|
|
7237
|
+
this.meridians = [];
|
|
7238
|
+
const { type, name, position, tilt } = props;
|
|
7239
|
+
this.type = type;
|
|
7240
|
+
this.name = name;
|
|
7241
|
+
this.position = new Vector3(position.x, position.y, position.z);
|
|
7242
|
+
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7243
|
+
this.incidentRays = [];
|
|
7244
|
+
this.refractedRays = [];
|
|
7245
|
+
}
|
|
7246
|
+
clearTraceHistory() {
|
|
7247
|
+
this.incidentRays = [];
|
|
7248
|
+
this.refractedRays = [];
|
|
7249
|
+
}
|
|
7250
|
+
getWorldPosition() {
|
|
7251
|
+
return this.position.clone();
|
|
7252
|
+
}
|
|
7253
|
+
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7254
|
+
this.position.copy(position);
|
|
7255
|
+
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7256
|
+
}
|
|
7257
|
+
/**
|
|
7258
|
+
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7259
|
+
*/
|
|
7260
|
+
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7261
|
+
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7262
|
+
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7263
|
+
this.position.copy(p1);
|
|
7264
|
+
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
|
|
7268
|
+
/**
|
|
7269
|
+
* 2D affine 왜곡 추정 전용 클래스입니다.
|
|
7270
|
+
* [x'; y'] = [[a b c], [d e f]] * [x y 1]^T 형태를 최소자승으로 적합합니다.
|
|
7271
|
+
*/
|
|
7272
|
+
class Affine {
|
|
7273
|
+
constructor() {
|
|
7274
|
+
this.lastResult = null;
|
|
7275
|
+
this.lastPairs = [];
|
|
7276
|
+
}
|
|
7277
|
+
estimate(pairs) {
|
|
7278
|
+
const inputPairs = Array.isArray(pairs) ? pairs : [];
|
|
7279
|
+
this.lastPairs = inputPairs;
|
|
7280
|
+
const affine = this.fitAffine2D(inputPairs);
|
|
7281
|
+
if (!affine) {
|
|
7282
|
+
this.lastResult = null;
|
|
7283
|
+
return null;
|
|
7284
|
+
}
|
|
7285
|
+
let residualSumPct = 0;
|
|
7286
|
+
let residualCount = 0;
|
|
7287
|
+
let residualMaxPct = 0;
|
|
7288
|
+
const residuals = [];
|
|
7289
|
+
for (const pair of inputPairs) {
|
|
7290
|
+
const px = affine.a * pair.sx + affine.b * pair.sy + affine.c;
|
|
7291
|
+
const py = affine.d * pair.sx + affine.e * pair.sy + affine.f;
|
|
7292
|
+
const rx = pair.tx - px;
|
|
7293
|
+
const ry = pair.ty - py;
|
|
7294
|
+
const magnitude = Math.hypot(rx, ry);
|
|
7295
|
+
if (magnitude < 1e-4)
|
|
7296
|
+
continue;
|
|
7297
|
+
const radiusRef = Math.hypot(px, py);
|
|
7298
|
+
const pct = (magnitude / Math.max(0.2, radiusRef)) * 100;
|
|
7299
|
+
residualSumPct += pct;
|
|
7300
|
+
residualCount += 1;
|
|
7301
|
+
residualMaxPct = Math.max(residualMaxPct, pct);
|
|
7302
|
+
residuals.push({ sx: pair.sx, sy: pair.sy, px, py, rx, ry, magnitude, pct });
|
|
7303
|
+
}
|
|
7304
|
+
const result = {
|
|
7305
|
+
...affine,
|
|
7306
|
+
count: inputPairs.length,
|
|
7307
|
+
residualAvgPct: residualCount ? residualSumPct / residualCount : 0,
|
|
7308
|
+
residualMaxPct,
|
|
7309
|
+
residuals,
|
|
7310
|
+
};
|
|
7311
|
+
this.lastResult = result;
|
|
7312
|
+
return result;
|
|
7313
|
+
}
|
|
7314
|
+
/**
|
|
7315
|
+
* 마지막 affine 추정 결과를 반환합니다.
|
|
7316
|
+
*/
|
|
7317
|
+
getLastResult() {
|
|
7318
|
+
return this.lastResult;
|
|
7319
|
+
}
|
|
7320
|
+
/**
|
|
7321
|
+
* 마지막 affine 추정에 사용된 입력쌍을 반환합니다.
|
|
7322
|
+
*/
|
|
7323
|
+
getLastPairs() {
|
|
7324
|
+
return [...this.lastPairs];
|
|
7325
|
+
}
|
|
7326
|
+
fitAffine2D(pairs) {
|
|
7327
|
+
if (!Array.isArray(pairs) || pairs.length < 4)
|
|
7328
|
+
return null;
|
|
7329
|
+
const ata = Array.from({ length: 6 }, () => Array(6).fill(0));
|
|
7330
|
+
const atb = Array(6).fill(0);
|
|
7331
|
+
const accumulate = (row, rhs) => {
|
|
7332
|
+
for (let i = 0; i < 6; i += 1) {
|
|
7333
|
+
atb[i] += row[i] * rhs;
|
|
7334
|
+
for (let j = 0; j < 6; j += 1)
|
|
7335
|
+
ata[i][j] += row[i] * row[j];
|
|
7336
|
+
}
|
|
7337
|
+
};
|
|
7338
|
+
for (const pair of pairs) {
|
|
7339
|
+
accumulate([pair.sx, pair.sy, 1, 0, 0, 0], pair.tx);
|
|
7340
|
+
accumulate([0, 0, 0, pair.sx, pair.sy, 1], pair.ty);
|
|
7341
|
+
}
|
|
7342
|
+
const n = 6;
|
|
7343
|
+
const aug = ata.map((row, index) => [...row, atb[index]]);
|
|
7344
|
+
for (let col = 0; col < n; col += 1) {
|
|
7345
|
+
let pivot = col;
|
|
7346
|
+
for (let row = col + 1; row < n; row += 1) {
|
|
7347
|
+
if (Math.abs(aug[row][col]) > Math.abs(aug[pivot][col]))
|
|
7348
|
+
pivot = row;
|
|
7349
|
+
}
|
|
7350
|
+
if (Math.abs(aug[pivot][col]) < 1e-10)
|
|
7351
|
+
return null;
|
|
7352
|
+
if (pivot !== col) {
|
|
7353
|
+
const temp = aug[col];
|
|
7354
|
+
aug[col] = aug[pivot];
|
|
7355
|
+
aug[pivot] = temp;
|
|
7356
|
+
}
|
|
7357
|
+
const divider = aug[col][col];
|
|
7358
|
+
for (let j = col; j <= n; j += 1)
|
|
7359
|
+
aug[col][j] /= divider;
|
|
7360
|
+
for (let row = 0; row < n; row += 1) {
|
|
7361
|
+
if (row === col)
|
|
7362
|
+
continue;
|
|
7363
|
+
const factor = aug[row][col];
|
|
7364
|
+
if (Math.abs(factor) < 1e-12)
|
|
7365
|
+
continue;
|
|
7366
|
+
for (let j = col; j <= n; j += 1)
|
|
7367
|
+
aug[row][j] -= factor * aug[col][j];
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
return {
|
|
7371
|
+
a: aug[0][n],
|
|
7372
|
+
b: aug[1][n],
|
|
7373
|
+
c: aug[2][n],
|
|
7374
|
+
d: aug[3][n],
|
|
7375
|
+
e: aug[4][n],
|
|
7376
|
+
f: aug[5][n],
|
|
7377
|
+
};
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7344
7381
|
/** 각막 기준 안구 회전점 z(mm). `SCAXEngineCore`와 동일. */
|
|
7345
7382
|
const EYE_ROTATION_PIVOT_FROM_CORNEA_MM = 13;
|
|
7346
7383
|
function normalizePrismAmount$2(value) {
|
|
@@ -7413,43 +7450,6 @@
|
|
|
7413
7450
|
}
|
|
7414
7451
|
}
|
|
7415
7452
|
|
|
7416
|
-
class Surface {
|
|
7417
|
-
constructor(props) {
|
|
7418
|
-
this.type = "";
|
|
7419
|
-
this.name = "";
|
|
7420
|
-
this.position = new Vector3(0, 0, 0);
|
|
7421
|
-
this.tilt = new Vector2(0, 0);
|
|
7422
|
-
this.meridians = [];
|
|
7423
|
-
const { type, name, position, tilt } = props;
|
|
7424
|
-
this.type = type;
|
|
7425
|
-
this.name = name;
|
|
7426
|
-
this.position = new Vector3(position.x, position.y, position.z);
|
|
7427
|
-
this.tilt = new Vector2(tilt.x, tilt.y);
|
|
7428
|
-
this.incidentRays = [];
|
|
7429
|
-
this.refractedRays = [];
|
|
7430
|
-
}
|
|
7431
|
-
clearTraceHistory() {
|
|
7432
|
-
this.incidentRays = [];
|
|
7433
|
-
this.refractedRays = [];
|
|
7434
|
-
}
|
|
7435
|
-
getWorldPosition() {
|
|
7436
|
-
return this.position.clone();
|
|
7437
|
-
}
|
|
7438
|
-
setPositionAndTilt(position, tiltXDeg, tiltYDeg) {
|
|
7439
|
-
this.position.copy(position);
|
|
7440
|
-
this.tilt.set(tiltXDeg, tiltYDeg);
|
|
7441
|
-
}
|
|
7442
|
-
/**
|
|
7443
|
-
* 안질 tilt가 0인 표면을 전제로, pivot 기준 강체 회전 후 동일 Euler(XYZ) tilt를 부여합니다.
|
|
7444
|
-
*/
|
|
7445
|
-
applyRigidRotationAboutPivot(pivot, rotation) {
|
|
7446
|
-
const p1 = pivot.clone().add(new Vector3().subVectors(this.position, pivot).applyQuaternion(rotation));
|
|
7447
|
-
const euler = new Euler().setFromQuaternion(rotation, "XYZ");
|
|
7448
|
-
this.position.copy(p1);
|
|
7449
|
-
this.tilt.set((euler.x * 180) / Math.PI, (euler.y * 180) / Math.PI);
|
|
7450
|
-
}
|
|
7451
|
-
}
|
|
7452
|
-
|
|
7453
7453
|
class ApertureStopSurface extends Surface {
|
|
7454
7454
|
constructor(props) {
|
|
7455
7455
|
super({
|
|
@@ -9629,8 +9629,8 @@
|
|
|
9629
9629
|
const orthogonalTabo = (taboAxis + 90) % 180;
|
|
9630
9630
|
const r = Math.hypot(j0, j45);
|
|
9631
9631
|
return [
|
|
9632
|
-
{ tabo: taboAxis, d: m
|
|
9633
|
-
{ tabo: orthogonalTabo, d: m
|
|
9632
|
+
{ tabo: taboAxis, d: m + r },
|
|
9633
|
+
{ tabo: orthogonalTabo, d: m - r },
|
|
9634
9634
|
].sort((a, b) => a.d - b.d);
|
|
9635
9635
|
}
|
|
9636
9636
|
calculateEyeRotationByPrism(prism) {
|
|
@@ -9645,9 +9645,11 @@
|
|
|
9645
9645
|
}
|
|
9646
9646
|
}
|
|
9647
9647
|
|
|
9648
|
+
exports.LightSource = LightSource;
|
|
9648
9649
|
exports.Ray = Ray;
|
|
9649
9650
|
exports.SCAXEngine = SCAXEngine;
|
|
9650
9651
|
exports.SCAXEngineCore = SCAXEngineCore;
|
|
9652
|
+
exports.Surface = Surface;
|
|
9651
9653
|
|
|
9652
9654
|
}));
|
|
9653
9655
|
//# sourceMappingURL=scax-engine.umd.js.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as Ray } from "./ray/ray";
|
|
2
|
+
export { LightSource } from "./light-sources/light-source";
|
|
3
|
+
export { default as Surface } from "./surfaces/surface";
|
|
2
4
|
export { default as SCAXEngine, SCAXEngineCore } from "./scax-engine";
|
|
3
|
-
export type { MeridianInfo, PrismPower, SCAXEngineProps, SCAXPower, SimulateResult, } from "./scax-engine";
|
|
5
|
+
export type { MeridianInfo, PrismPower, SCAXEngineProps, SCAXPower, SimulateResult, SturmResult } from "./scax-engine";
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,6 +2,7 @@ import Affine from "./affine/affine";
|
|
|
2
2
|
import { GridLightSourceProps, GridRGLightSourceProps, LightSource, RadialLightSourceProps } from "./light-sources/light-source";
|
|
3
3
|
import { EyeModelParameter } from "./parameters/eye/eyemodel-parameter";
|
|
4
4
|
import Ray from "./ray/ray";
|
|
5
|
+
import Sturm from "./sturm/sturm";
|
|
5
6
|
import Surface from "./surfaces/surface";
|
|
6
7
|
export type EyeModel = "gullstrand" | "navarro";
|
|
7
8
|
export type PupilType = "constricted" | "neutral" | "dilated" | "none";
|
|
@@ -95,6 +96,8 @@ export type MeridianInfo = {
|
|
|
95
96
|
tabo: number;
|
|
96
97
|
d: number;
|
|
97
98
|
}[];
|
|
99
|
+
/** `Sturm#calculate()` / `SCAXEngine#sturmCalculation()` 반환 구조 */
|
|
100
|
+
export type SturmResult = ReturnType<Sturm["calculate"]>;
|
|
98
101
|
export type AffineAnalysisResult = ReturnType<Affine["estimate"]>;
|
|
99
102
|
/**
|
|
100
103
|
* legacy simulator.js를 TypeScript로 옮긴 핵심 시뮬레이터입니다.
|
|
@@ -143,117 +146,7 @@ export declare class SCAXEngineCore {
|
|
|
143
146
|
* 2) Sturm calculation 전용 함수
|
|
144
147
|
* traced ray 집합에서 z-scan 기반 Sturm 슬라이스/근사 중심을 계산합니다.
|
|
145
148
|
*/
|
|
146
|
-
sturmCalculation(rays?: Ray[]):
|
|
147
|
-
slices_info: {
|
|
148
|
-
count: number;
|
|
149
|
-
slices: {
|
|
150
|
-
z: number;
|
|
151
|
-
depth: number;
|
|
152
|
-
ratio: number;
|
|
153
|
-
size: number;
|
|
154
|
-
profile: {
|
|
155
|
-
at: {
|
|
156
|
-
x: number;
|
|
157
|
-
y: number;
|
|
158
|
-
z: number;
|
|
159
|
-
};
|
|
160
|
-
wMajor: number;
|
|
161
|
-
wMinor: number;
|
|
162
|
-
angleMajorDeg: number;
|
|
163
|
-
j0: number;
|
|
164
|
-
j45: number;
|
|
165
|
-
angleMinorDeg: number;
|
|
166
|
-
majorDirection: {
|
|
167
|
-
x: number;
|
|
168
|
-
y: number;
|
|
169
|
-
z: number;
|
|
170
|
-
};
|
|
171
|
-
minorDirection: {
|
|
172
|
-
x: number;
|
|
173
|
-
y: number;
|
|
174
|
-
z: number;
|
|
175
|
-
};
|
|
176
|
-
};
|
|
177
|
-
}[];
|
|
178
|
-
};
|
|
179
|
-
sturm_info: {
|
|
180
|
-
has_astigmatism: boolean;
|
|
181
|
-
method: string;
|
|
182
|
-
anterior: {
|
|
183
|
-
z: number;
|
|
184
|
-
depth: number;
|
|
185
|
-
ratio: number;
|
|
186
|
-
size: number;
|
|
187
|
-
profile: {
|
|
188
|
-
at: {
|
|
189
|
-
x: number;
|
|
190
|
-
y: number;
|
|
191
|
-
z: number;
|
|
192
|
-
};
|
|
193
|
-
wMajor: number;
|
|
194
|
-
wMinor: number;
|
|
195
|
-
angleMajorDeg: number;
|
|
196
|
-
j0: number;
|
|
197
|
-
j45: number;
|
|
198
|
-
angleMinorDeg: number;
|
|
199
|
-
majorDirection: {
|
|
200
|
-
x: number;
|
|
201
|
-
y: number;
|
|
202
|
-
z: number;
|
|
203
|
-
};
|
|
204
|
-
minorDirection: {
|
|
205
|
-
x: number;
|
|
206
|
-
y: number;
|
|
207
|
-
z: number;
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
};
|
|
211
|
-
posterior: {
|
|
212
|
-
z: number;
|
|
213
|
-
depth: number;
|
|
214
|
-
ratio: number;
|
|
215
|
-
size: number;
|
|
216
|
-
profile: {
|
|
217
|
-
at: {
|
|
218
|
-
x: number;
|
|
219
|
-
y: number;
|
|
220
|
-
z: number;
|
|
221
|
-
};
|
|
222
|
-
wMajor: number;
|
|
223
|
-
wMinor: number;
|
|
224
|
-
angleMajorDeg: number;
|
|
225
|
-
j0: number;
|
|
226
|
-
j45: number;
|
|
227
|
-
angleMinorDeg: number;
|
|
228
|
-
majorDirection: {
|
|
229
|
-
x: number;
|
|
230
|
-
y: number;
|
|
231
|
-
z: number;
|
|
232
|
-
};
|
|
233
|
-
minorDirection: {
|
|
234
|
-
x: number;
|
|
235
|
-
y: number;
|
|
236
|
-
z: number;
|
|
237
|
-
};
|
|
238
|
-
};
|
|
239
|
-
} | null;
|
|
240
|
-
approx_center: {
|
|
241
|
-
x: number;
|
|
242
|
-
y: number;
|
|
243
|
-
z: number;
|
|
244
|
-
mode: string;
|
|
245
|
-
} | null;
|
|
246
|
-
line: "g" | "F" | "e" | "d" | "C" | "r";
|
|
247
|
-
wavelength_nm: number;
|
|
248
|
-
color: number | null;
|
|
249
|
-
ray_count: number;
|
|
250
|
-
analysis_axis: {
|
|
251
|
-
x: number;
|
|
252
|
-
y: number;
|
|
253
|
-
z: number;
|
|
254
|
-
};
|
|
255
|
-
}[];
|
|
256
|
-
};
|
|
149
|
+
sturmCalculation(rays?: Ray[]): SturmResult;
|
|
257
150
|
getAffineAnalysis(): AffineAnalysisResult;
|
|
258
151
|
private surfaceOrderZ;
|
|
259
152
|
private readSurfacePosition;
|
|
@@ -279,117 +172,7 @@ export default class SCAXEngine {
|
|
|
279
172
|
dispose(): void;
|
|
280
173
|
simulate(): SimulateResult;
|
|
281
174
|
rayTracing(): Ray[];
|
|
282
|
-
sturmCalculation(rays?: Ray[]):
|
|
283
|
-
slices_info: {
|
|
284
|
-
count: number;
|
|
285
|
-
slices: {
|
|
286
|
-
z: number;
|
|
287
|
-
depth: number;
|
|
288
|
-
ratio: number;
|
|
289
|
-
size: number;
|
|
290
|
-
profile: {
|
|
291
|
-
at: {
|
|
292
|
-
x: number;
|
|
293
|
-
y: number;
|
|
294
|
-
z: number;
|
|
295
|
-
};
|
|
296
|
-
wMajor: number;
|
|
297
|
-
wMinor: number;
|
|
298
|
-
angleMajorDeg: number;
|
|
299
|
-
j0: number;
|
|
300
|
-
j45: number;
|
|
301
|
-
angleMinorDeg: number;
|
|
302
|
-
majorDirection: {
|
|
303
|
-
x: number;
|
|
304
|
-
y: number;
|
|
305
|
-
z: number;
|
|
306
|
-
};
|
|
307
|
-
minorDirection: {
|
|
308
|
-
x: number;
|
|
309
|
-
y: number;
|
|
310
|
-
z: number;
|
|
311
|
-
};
|
|
312
|
-
};
|
|
313
|
-
}[];
|
|
314
|
-
};
|
|
315
|
-
sturm_info: {
|
|
316
|
-
has_astigmatism: boolean;
|
|
317
|
-
method: string;
|
|
318
|
-
anterior: {
|
|
319
|
-
z: number;
|
|
320
|
-
depth: number;
|
|
321
|
-
ratio: number;
|
|
322
|
-
size: number;
|
|
323
|
-
profile: {
|
|
324
|
-
at: {
|
|
325
|
-
x: number;
|
|
326
|
-
y: number;
|
|
327
|
-
z: number;
|
|
328
|
-
};
|
|
329
|
-
wMajor: number;
|
|
330
|
-
wMinor: number;
|
|
331
|
-
angleMajorDeg: number;
|
|
332
|
-
j0: number;
|
|
333
|
-
j45: number;
|
|
334
|
-
angleMinorDeg: number;
|
|
335
|
-
majorDirection: {
|
|
336
|
-
x: number;
|
|
337
|
-
y: number;
|
|
338
|
-
z: number;
|
|
339
|
-
};
|
|
340
|
-
minorDirection: {
|
|
341
|
-
x: number;
|
|
342
|
-
y: number;
|
|
343
|
-
z: number;
|
|
344
|
-
};
|
|
345
|
-
};
|
|
346
|
-
};
|
|
347
|
-
posterior: {
|
|
348
|
-
z: number;
|
|
349
|
-
depth: number;
|
|
350
|
-
ratio: number;
|
|
351
|
-
size: number;
|
|
352
|
-
profile: {
|
|
353
|
-
at: {
|
|
354
|
-
x: number;
|
|
355
|
-
y: number;
|
|
356
|
-
z: number;
|
|
357
|
-
};
|
|
358
|
-
wMajor: number;
|
|
359
|
-
wMinor: number;
|
|
360
|
-
angleMajorDeg: number;
|
|
361
|
-
j0: number;
|
|
362
|
-
j45: number;
|
|
363
|
-
angleMinorDeg: number;
|
|
364
|
-
majorDirection: {
|
|
365
|
-
x: number;
|
|
366
|
-
y: number;
|
|
367
|
-
z: number;
|
|
368
|
-
};
|
|
369
|
-
minorDirection: {
|
|
370
|
-
x: number;
|
|
371
|
-
y: number;
|
|
372
|
-
z: number;
|
|
373
|
-
};
|
|
374
|
-
};
|
|
375
|
-
} | null;
|
|
376
|
-
approx_center: {
|
|
377
|
-
x: number;
|
|
378
|
-
y: number;
|
|
379
|
-
z: number;
|
|
380
|
-
mode: string;
|
|
381
|
-
} | null;
|
|
382
|
-
line: "g" | "F" | "e" | "d" | "C" | "r";
|
|
383
|
-
wavelength_nm: number;
|
|
384
|
-
color: number | null;
|
|
385
|
-
ray_count: number;
|
|
386
|
-
analysis_axis: {
|
|
387
|
-
x: number;
|
|
388
|
-
y: number;
|
|
389
|
-
z: number;
|
|
390
|
-
};
|
|
391
|
-
}[];
|
|
392
|
-
};
|
|
175
|
+
sturmCalculation(rays?: Ray[]): SturmResult;
|
|
393
176
|
calculateMeridians(scaxPowers: SCAXPower[]): MeridianInfo;
|
|
394
177
|
calculateEyeRotationByPrism(prism: PrismPower): {
|
|
395
178
|
x: number;
|
package/package.json
CHANGED