@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 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;