@willcgage/module-schematic 0.83.2 → 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
@@ -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;