@willcgage/module-schematic 0.83.1 → 0.84.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
@@ -1651,7 +1651,7 @@ var FAST_TRACKS_N_ME55_CROSSOVERS = [
1651
1651
  secondaryFrogAngle: {
1652
1652
  deg: second,
1653
1653
  source: manufacturer,
1654
- note: `${spec}. The diamond of a scissors crossover \u2014 2 \xD7 ${deg}\xB0 within the published rounding.`
1654
+ note: `${spec}. The SCISSORS of a double crossover \u2014 the X where its two opposite diverging routes cross \u2014 at 2 \xD7 ${deg}\xB0 within the published rounding.`
1655
1655
  },
1656
1656
  overallLength: {
1657
1657
  inches: dflt,
@@ -2128,6 +2128,107 @@ function turnoutClosure(size, opts = {}) {
2128
2128
  }
2129
2129
  };
2130
2130
  }
2131
+ function partGeometryGap(part) {
2132
+ if (part.kind === "flex") return null;
2133
+ if (part.kind === "crossover")
2134
+ return "a crossover fixture builds one HALF of the assembly (piecesPerAssembly), so its published lengths describe a piece, not the finished part \u2014 the geometry of the whole crossover is not yet derivable from them";
2135
+ if (part.kind === "crossing") return "crossing geometry is not modelled yet";
2136
+ if (part.kind === "curved-turnout")
2137
+ return "a curved turnout needs both radii AND its points/frog landmarks; only the radii are published";
2138
+ if (!part.pointsOffset)
2139
+ return "no points offset \u2014 without it there is nowhere for the diverging route to begin";
2140
+ if (!part.overallLength) return "no overall length \u2014 the part has no end to put a joint on";
2141
+ if (part.frogNumber == null) return "no frog number \u2014 the diverging angle is unknown";
2142
+ return null;
2143
+ }
2144
+ function partGeometry(part, library = BUILT_IN_TRACK_PARTS) {
2145
+ if (partGeometryGap(part)) return null;
2146
+ if (part.kind === "flex") {
2147
+ return {
2148
+ joints: [
2149
+ { id: "a", role: "throat", x: 0, y: 0, angleDeg: 180 },
2150
+ { id: "b", role: "through", x: 0, y: 0, angleDeg: 0 }
2151
+ ],
2152
+ routes: [["a", "b"]],
2153
+ source: "derived",
2154
+ divergingEndMeasured: false
2155
+ };
2156
+ }
2157
+ const N = part.frogNumber;
2158
+ const points = part.pointsOffset;
2159
+ const overall = part.overallLength;
2160
+ const frog = part.frogOffset;
2161
+ const lead = part.lead?.inches ?? (frog ? frog.inches - points.inches : void 0);
2162
+ if (lead == null || !(lead > 0)) return null;
2163
+ const isWye = part.kind === "wye";
2164
+ const effN = isWye ? N * 2 : N;
2165
+ const closure = turnoutClosure(effN, { leadInches: lead });
2166
+ const divergingEnd = () => {
2167
+ const frogX = frog ? frog.inches : points.inches + lead;
2168
+ const slope = closure.frogSlope;
2169
+ const dir = 1 / Math.hypot(1, slope);
2170
+ if (part.divergingLength) {
2171
+ const L = part.divergingLength.inches;
2172
+ return {
2173
+ x: frogX + L * dir,
2174
+ y: closure.offsetAt(frogX - points.inches) + L * slope * dir,
2175
+ measured: true
2176
+ };
2177
+ }
2178
+ return {
2179
+ x: overall.inches,
2180
+ y: closure.offsetAt(overall.inches - points.inches),
2181
+ measured: false
2182
+ };
2183
+ };
2184
+ const weakest = (...ds) => {
2185
+ const rank = ["measured", "manufacturer", "derived", "unverified"];
2186
+ let worst = 0;
2187
+ for (const d of ds) if (d) worst = Math.max(worst, rank.indexOf(d.source));
2188
+ return rank[worst];
2189
+ };
2190
+ const de = divergingEnd();
2191
+ const legAngle = Math.atan(closure.frogSlope) * 180 / Math.PI;
2192
+ if (isWye) {
2193
+ return {
2194
+ joints: [
2195
+ { id: "throat", role: "throat", x: 0, y: 0, angleDeg: 180 },
2196
+ { id: "legA", role: "diverge", x: de.x, y: de.y, angleDeg: legAngle },
2197
+ { id: "legB", role: "divergeB", x: de.x, y: -de.y, angleDeg: -legAngle }
2198
+ ],
2199
+ routes: [
2200
+ ["throat", "legA"],
2201
+ ["throat", "legB"]
2202
+ ],
2203
+ source: weakest(points, overall, frog, part.divergingLength),
2204
+ divergingEndMeasured: de.measured
2205
+ };
2206
+ }
2207
+ return {
2208
+ joints: [
2209
+ { id: "throat", role: "throat", x: 0, y: 0, angleDeg: 180 },
2210
+ { id: "through", role: "through", x: overall.inches, y: 0, angleDeg: 0 },
2211
+ { id: "diverge", role: "diverge", x: de.x, y: de.y, angleDeg: legAngle }
2212
+ ],
2213
+ routes: [
2214
+ ["throat", "through"],
2215
+ ["throat", "diverge"]
2216
+ ],
2217
+ source: weakest(points, overall, frog, part.divergingLength),
2218
+ divergingEndMeasured: de.measured
2219
+ };
2220
+ }
2221
+ function partsPlaceable(library = BUILT_IN_TRACK_PARTS) {
2222
+ const placeable = [];
2223
+ const blocked = [];
2224
+ for (const p of library) {
2225
+ const why = partGeometryGap(p);
2226
+ if (why) blocked.push({ part: p, why });
2227
+ else if (partGeometry(p, library)) placeable.push(p);
2228
+ else blocked.push({ part: p, why: "dimensions present but inconsistent" });
2229
+ }
2230
+ return { placeable, blocked };
2231
+ }
2131
2232
  function frogCasting(cl, opts = {}) {
2132
2233
  const g = opts.gaugeInches ?? RAIL_GAUGE_INCHES;
2133
2234
  const w = opts.reachInches ?? g * 1.5;
@@ -2778,7 +2879,10 @@ exports.nextId = nextId;
2778
2879
  exports.parseXtpLibrary = parseXtpLibrary;
2779
2880
  exports.partExtent = partExtent;
2780
2881
  exports.partExtentForSize = partExtentForSize;
2882
+ exports.partGeometry = partGeometry;
2883
+ exports.partGeometryGap = partGeometryGap;
2781
2884
  exports.partOutlineAtFrog = partOutlineAtFrog;
2885
+ exports.partsPlaceable = partsPlaceable;
2782
2886
  exports.pastFrogInchesForSize = pastFrogInchesForSize;
2783
2887
  exports.pathLengthInches = pathLengthInches;
2784
2888
  exports.poseNeedsManual = poseNeedsManual;