@willcgage/module-schematic 0.56.0 → 0.58.0
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/index.cjs +125 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +122 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -769,7 +769,8 @@ function stateToDoc(state, recordNumber) {
|
|
|
769
769
|
name: t.name || void 0,
|
|
770
770
|
...t.size ? { size: t.size } : {},
|
|
771
771
|
...t.curved ? { curved: true } : {},
|
|
772
|
-
...t.flipped ? { flipped: true } : {}
|
|
772
|
+
...t.flipped ? { flipped: true } : {},
|
|
773
|
+
...t.partId ? { partId: t.partId } : {}
|
|
773
774
|
})),
|
|
774
775
|
...state.crossings.length > 0 ? {
|
|
775
776
|
crossings: state.crossings.map((x) => ({
|
|
@@ -957,7 +958,8 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
|
|
|
957
958
|
kind: t.kind ?? "right",
|
|
958
959
|
...t.size ? { size: t.size } : {},
|
|
959
960
|
...t.curved ? { curved: true } : {},
|
|
960
|
-
...t.flipped ? { flipped: true } : {}
|
|
961
|
+
...t.flipped ? { flipped: true } : {},
|
|
962
|
+
...t.partId ? { partId: t.partId } : {}
|
|
961
963
|
})),
|
|
962
964
|
controlPoints: readControlPoints(d, sc),
|
|
963
965
|
industries: (d.industries ?? []).map((ind) => ({
|
|
@@ -1320,6 +1322,123 @@ function samplePartSegments(segments, stepsPerCurve = 16) {
|
|
|
1320
1322
|
return out;
|
|
1321
1323
|
});
|
|
1322
1324
|
}
|
|
1325
|
+
function angleDeltaDeg(a, b) {
|
|
1326
|
+
let d = ((a - b) % 360 + 540) % 360 - 180;
|
|
1327
|
+
if (d === -180) d = 180;
|
|
1328
|
+
return d;
|
|
1329
|
+
}
|
|
1330
|
+
function frogNumberFromName(name) {
|
|
1331
|
+
if (!name) return void 0;
|
|
1332
|
+
const m = name.match(/(?:#|no\.?\s*|number\s+)(\d+(?:\.\d+)?)/i);
|
|
1333
|
+
if (!m) return void 0;
|
|
1334
|
+
const n = Number(m[1]);
|
|
1335
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
1336
|
+
}
|
|
1337
|
+
function importedKind(name) {
|
|
1338
|
+
const s = (name ?? "").toLowerCase();
|
|
1339
|
+
if (s.includes("wye")) return "wye";
|
|
1340
|
+
if (s.includes("crossing")) return "crossing";
|
|
1341
|
+
if (s.includes("curved")) return "curved-turnout";
|
|
1342
|
+
return "turnout";
|
|
1343
|
+
}
|
|
1344
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1345
|
+
function importedPartToTrackPart(part, sourceName = "imported .xtp") {
|
|
1346
|
+
const name = part.name ?? part.title ?? "Imported part";
|
|
1347
|
+
const manufacturer = part.manufacturer ?? "Unknown";
|
|
1348
|
+
const note = `from ${sourceName} \u2014 community CAD data, not a measurement`;
|
|
1349
|
+
const out = {
|
|
1350
|
+
id: `xtp-${slug(manufacturer)}-${slug(part.partNumber || name)}`,
|
|
1351
|
+
manufacturer,
|
|
1352
|
+
name,
|
|
1353
|
+
line: "",
|
|
1354
|
+
scale: "N",
|
|
1355
|
+
kind: importedKind(name),
|
|
1356
|
+
frogNumber: frogNumberFromName(name),
|
|
1357
|
+
segments: part.segments,
|
|
1358
|
+
ends: part.ends,
|
|
1359
|
+
importedFrom: sourceName
|
|
1360
|
+
};
|
|
1361
|
+
if (part.partNumber) out.partNumbers = { single: part.partNumber };
|
|
1362
|
+
const ends = part.ends;
|
|
1363
|
+
if (ends.length >= 2) {
|
|
1364
|
+
const a = ends[0].angleDeg * Math.PI / 180;
|
|
1365
|
+
const ux = Math.sin(a);
|
|
1366
|
+
const uy = Math.cos(a);
|
|
1367
|
+
const proj = ends.map((e) => (e.x - ends[0].x) * ux + (e.y - ends[0].y) * uy);
|
|
1368
|
+
const span = Math.max(...proj) - Math.min(...proj);
|
|
1369
|
+
if (span > 0) out.overallLength = { inches: span, source: "unverified", note };
|
|
1370
|
+
}
|
|
1371
|
+
if (ends.length >= 3) {
|
|
1372
|
+
const rest = ends.slice(1);
|
|
1373
|
+
const opposite = rest.map((e) => ({ e, off: Math.abs(angleDeltaDeg(e.angleDeg, ends[0].angleDeg + 180)) })).sort((p, q) => p.off - q.off);
|
|
1374
|
+
const through = opposite[0].e;
|
|
1375
|
+
const diverging = opposite[opposite.length - 1].e;
|
|
1376
|
+
const deg = Math.abs(angleDeltaDeg(diverging.angleDeg, through.angleDeg));
|
|
1377
|
+
if (deg > 0.01) out.actualAngle = { deg, source: "unverified", note };
|
|
1378
|
+
}
|
|
1379
|
+
return out;
|
|
1380
|
+
}
|
|
1381
|
+
function mergeImportedParts(imported, library = BUILT_IN_TRACK_PARTS, sourceName = "imported .xtp") {
|
|
1382
|
+
const out = library.map((p) => ({ ...p }));
|
|
1383
|
+
const numbersOf = (p) => [p.partNumbers?.left, p.partNumbers?.right, p.partNumbers?.single].filter(Boolean).map((s) => s.trim().toLowerCase());
|
|
1384
|
+
for (const raw of imported) {
|
|
1385
|
+
const conv = importedPartToTrackPart(raw, sourceName);
|
|
1386
|
+
const pn = raw.partNumber?.trim().toLowerCase();
|
|
1387
|
+
const match = (pn ? out.find((p) => numbersOf(p).includes(pn)) : void 0) ?? (conv.frogNumber != null ? out.find(
|
|
1388
|
+
(p) => p.frogNumber === conv.frogNumber && p.kind === conv.kind && p.manufacturer.toLowerCase() === conv.manufacturer.toLowerCase()
|
|
1389
|
+
) : void 0);
|
|
1390
|
+
if (!match) {
|
|
1391
|
+
out.push(conv);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (!match.segments?.length && conv.segments?.length) match.segments = conv.segments;
|
|
1395
|
+
if (!match.ends?.length && conv.ends?.length) match.ends = conv.ends;
|
|
1396
|
+
if (!match.importedFrom && (conv.segments?.length || conv.ends?.length)) {
|
|
1397
|
+
match.importedFrom = sourceName;
|
|
1398
|
+
}
|
|
1399
|
+
if (!match.overallLength && conv.overallLength) match.overallLength = conv.overallLength;
|
|
1400
|
+
if (!match.actualAngle && conv.actualAngle) match.actualAngle = conv.actualAngle;
|
|
1401
|
+
}
|
|
1402
|
+
return out;
|
|
1403
|
+
}
|
|
1404
|
+
function partOutlineAtFrog(part, leadInches, stepsPerCurve = 16) {
|
|
1405
|
+
const ends = part.ends ?? [];
|
|
1406
|
+
const segs = part.segments ?? [];
|
|
1407
|
+
if (!segs.length || ends.length < 2) return null;
|
|
1408
|
+
let points = ends[0];
|
|
1409
|
+
if (ends.length >= 3) {
|
|
1410
|
+
let bestPair = Infinity;
|
|
1411
|
+
let pairIdx = [1, 2];
|
|
1412
|
+
for (let i = 0; i < ends.length; i++) {
|
|
1413
|
+
for (let j = i + 1; j < ends.length; j++) {
|
|
1414
|
+
const d = Math.hypot(ends[i].x - ends[j].x, ends[i].y - ends[j].y);
|
|
1415
|
+
if (d < bestPair) {
|
|
1416
|
+
bestPair = d;
|
|
1417
|
+
pairIdx = [i, j];
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const odd = ends.findIndex((_, k) => k !== pairIdx[0] && k !== pairIdx[1]);
|
|
1422
|
+
if (odd >= 0) points = ends[odd];
|
|
1423
|
+
}
|
|
1424
|
+
const a = points.angleDeg * Math.PI / 180;
|
|
1425
|
+
const ux = -Math.sin(a);
|
|
1426
|
+
const uy = -Math.cos(a);
|
|
1427
|
+
const toLocal = (p) => {
|
|
1428
|
+
const dx = p.x - points.x;
|
|
1429
|
+
const dy = p.y - points.y;
|
|
1430
|
+
return { x: dx * ux + dy * uy, y: dx * -uy + dy * ux };
|
|
1431
|
+
};
|
|
1432
|
+
let sign = 1;
|
|
1433
|
+
const offAxis = ends.filter((e) => e !== points).map(toLocal).sort((p, q) => Math.abs(q.y) - Math.abs(p.y))[0];
|
|
1434
|
+
if (offAxis && offAxis.y < 0) sign = -1;
|
|
1435
|
+
return samplePartSegments(segs, stepsPerCurve).map(
|
|
1436
|
+
(poly) => poly.map((p) => {
|
|
1437
|
+
const l = toLocal(p);
|
|
1438
|
+
return { x: l.x - leadInches, y: l.y * sign };
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1323
1442
|
function turnoutClosure(size, opts = {}) {
|
|
1324
1443
|
const N = size > 0 ? size : 6;
|
|
1325
1444
|
const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
|
|
@@ -1882,12 +2001,15 @@ exports.endplateLead = endplateLead;
|
|
|
1882
2001
|
exports.endplateTrackOffsetFor = endplateTrackOffsetFor;
|
|
1883
2002
|
exports.endplateTrackOffsetInches = endplateTrackOffsetInches;
|
|
1884
2003
|
exports.endplateWidthInches = endplateWidthInches;
|
|
2004
|
+
exports.frogNumberFromName = frogNumberFromName;
|
|
1885
2005
|
exports.fromSectionRelative = fromSectionRelative;
|
|
1886
2006
|
exports.geometryTurnDegrees = geometryTurnDegrees;
|
|
2007
|
+
exports.importedPartToTrackPart = importedPartToTrackPart;
|
|
1887
2008
|
exports.inchesToScaleFeet = inchesToScaleFeet;
|
|
1888
2009
|
exports.isLoopDoc = isLoopDoc;
|
|
1889
2010
|
exports.isTransitionTurnout = isTransitionTurnout;
|
|
1890
2011
|
exports.leadInchesForSize = leadInchesForSize;
|
|
2012
|
+
exports.mergeImportedParts = mergeImportedParts;
|
|
1891
2013
|
exports.moduleCenterline = moduleCenterline;
|
|
1892
2014
|
exports.moduleFeatures = moduleFeatures;
|
|
1893
2015
|
exports.moduleFootprint = moduleFootprint;
|
|
@@ -1895,6 +2017,7 @@ exports.moduleLengthFromSections = moduleLengthFromSections;
|
|
|
1895
2017
|
exports.moduleSections = moduleSections;
|
|
1896
2018
|
exports.nextId = nextId;
|
|
1897
2019
|
exports.parseXtpLibrary = parseXtpLibrary;
|
|
2020
|
+
exports.partOutlineAtFrog = partOutlineAtFrog;
|
|
1898
2021
|
exports.poseNeedsManual = poseNeedsManual;
|
|
1899
2022
|
exports.poseOverridesFromDoc = poseOverridesFromDoc;
|
|
1900
2023
|
exports.remapPos = remapPos;
|