@willcgage/module-schematic 0.16.0 → 0.18.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.d.cts CHANGED
@@ -132,6 +132,35 @@ interface SchematicControlPoint {
132
132
  crossings?: string[];
133
133
  signals?: SchematicSignal[];
134
134
  }
135
+ /** What a rendered industry shows beside its name: a car count, a length in
136
+ * inches, or nothing (name only). Authored per industry. */
137
+ type IndustryLabelMode = "none" | "cars" | "inches";
138
+ /**
139
+ * An industry — a rail-served customer that spots cars, authored as a SPAN on a
140
+ * track (a spur/siding, or the main). Positional like everything else: it lives
141
+ * in the same module-local inch frame and is rendered into the shared 2-D view,
142
+ * offset to `side`. The span length gives its car capacity; the dispatcher and
143
+ * crews read where cars set out. Mirrors a `freemon_industries` row.
144
+ */
145
+ interface SchematicIndustry {
146
+ id: string;
147
+ name: string;
148
+ /** Industry type value from the lookup (e.g. "team_track", "grain"). */
149
+ type?: string | null;
150
+ /** The track this industry spots cars on (a spur/siding id, or the main). */
151
+ track: string;
152
+ /** The car-spot span along that track, inches from endplate A. */
153
+ fromPos: number;
154
+ toPos: number;
155
+ /** Which side of the track the building + label sit on. */
156
+ side?: SignalSide;
157
+ /** Secondary readout at the label — a car count, a length, or none. */
158
+ labelMode?: IndustryLabelMode;
159
+ /** Car types this industry receives (car-type value strings). */
160
+ carTypes?: string[];
161
+ /** The `freemon_industries` row this is (single source of truth); null = new. */
162
+ moduleIndustryId?: number | null;
163
+ }
135
164
  interface ModuleSchematicDoc {
136
165
  version: number;
137
166
  module?: string;
@@ -156,6 +185,8 @@ interface ModuleSchematicDoc {
156
185
  /** Grade crossings / diamonds (#170). */
157
186
  crossings?: SchematicCrossing[];
158
187
  controlPoints?: SchematicControlPoint[];
188
+ /** Rail-served industries — car-spot spans on a track (#industries). */
189
+ industries?: SchematicIndustry[];
159
190
  /** Benchwork FOOTPRINT outline — the module's physical board shape as a
160
191
  * polygon in module-local inches, in the same frame as the endplate poses
161
192
  * (endplate A's track point at the origin, the mainline along +x, perpendicular
@@ -191,6 +222,45 @@ declare function sampleBenchworkOutline(pts: BenchworkPoint[], segsPerArc?: numb
191
222
  x: number;
192
223
  y: number;
193
224
  }[];
225
+ interface ModuleFootprintInput {
226
+ /** Mainline length (falls back to footprint length), inches. */
227
+ lengthInches: number;
228
+ geometryType?: string | null;
229
+ geometryDegrees?: number | null;
230
+ geometryOffsetInches?: number | null;
231
+ /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
232
+ endplateWidths?: Record<string, number>;
233
+ /** Authored benchwork outline (module-local inches), or absent for the band. */
234
+ outline?: BenchworkPoint[] | null;
235
+ }
236
+ interface OutlineFace {
237
+ /** The endplate face's two corners + midpoint (the track point). */
238
+ p1: BenchworkPoint;
239
+ p2: BenchworkPoint;
240
+ mid: BenchworkPoint;
241
+ }
242
+ interface ModuleFootprint {
243
+ /** Main track centre-line A→B (arcs sampled). */
244
+ centerline: BenchworkPoint[];
245
+ /** Derived benchwork band (endplate-width ribbon); the outline fallback. */
246
+ band: BenchworkPoint[];
247
+ /** Endplate faces: [A end, B end]. */
248
+ endplateFaces: OutlineFace[];
249
+ /** Authored outline (arc-sampled closed ring) or null → render the band. */
250
+ outline: BenchworkPoint[] | null;
251
+ }
252
+ /** Module-local main track centre-line (A→B), sampling arcs for curves/corners. */
253
+ declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
254
+ /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
255
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
256
+ /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
257
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
258
+ /**
259
+ * The full single-module physical footprint: centre-line + derived band +
260
+ * endplate faces + the authored outline (arc-sampled), all in module-local
261
+ * inches. Renderers draw `outline ?? band`.
262
+ */
263
+ declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
194
264
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
195
265
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
196
266
  declare const MAIN_TRACK_ID = "main";
@@ -204,6 +274,13 @@ declare const N_SCALE_RATIO = 160;
204
274
  declare function inchesToScaleFeet(inches: number, ratio?: number): number;
205
275
  /** Scale feet of prototype track → real inches on the module. */
206
276
  declare function scaleFeetToInches(feet: number, ratio?: number): number;
277
+ /** Length a spotted car occupies on N-scale track, inches. A 40-ft car body is
278
+ * ~3.0″; ~3.3″ over the couplers — the real spacing a cut of cars takes. The
279
+ * single constant every repo reads so a track's car count matches everywhere. */
280
+ declare const N_CAR_LENGTH_INCHES = 3.3;
281
+ /** How many cars fit in a span, from its drawn length — the derived capacity a
282
+ * siding or an industry spot holds (never typed). */
283
+ declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
207
284
  /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
208
285
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
209
286
  interface EditorTrack {
@@ -255,6 +332,20 @@ interface EditorCrossing {
255
332
  trackA: string;
256
333
  trackB: string;
257
334
  }
335
+ /** An industry as the authoring form binds it — a car-spot span on a track. */
336
+ interface EditorIndustry {
337
+ id: string;
338
+ name: string;
339
+ type: string;
340
+ track: string;
341
+ fromPos: number;
342
+ toPos: number;
343
+ side: SignalSide;
344
+ labelMode: IndustryLabelMode;
345
+ carTypes: string[];
346
+ /** freemon_industries row (single source of truth), or null for a new one. */
347
+ moduleIndustryId: number | null;
348
+ }
258
349
  /** A 3rd+ endplate — a branch/junction connection off the module (#170).
259
350
  * A module may have several (e.g. a set carrying a second railroad through:
260
351
  * MoPac enters at one branch endplate and leaves at another). */
@@ -300,6 +391,8 @@ interface EditorState {
300
391
  * Empty = no authored outline (fall back to the endplate-width band). */
301
392
  outline: BenchworkPoint[];
302
393
  controlPoints: EditorControlPoint[];
394
+ /** Rail-served industries — car-spot spans on a track (#industries). */
395
+ industries: EditorIndustry[];
303
396
  }
304
397
  /** Build the empty editor state for a module of the given length. */
305
398
  declare function emptyEditorState(lengthInches: number): EditorState;
@@ -430,6 +523,23 @@ interface BranchConnector {
430
523
  posFrac: number;
431
524
  side: "up" | "down";
432
525
  }
526
+ /** An industry — draw a car-spot span beside its track's lane, on `side`, with
527
+ * a name label + an optional car/length readout (#industries). */
528
+ interface DrawIndustry {
529
+ id: string;
530
+ name: string;
531
+ type: string | null;
532
+ /** Span as fractions of module length (sorted West→East). */
533
+ fromFrac: number;
534
+ toFrac: number;
535
+ /** Lane of the track it spots on, so it draws beside the right track. */
536
+ lane: number;
537
+ side: SignalSide;
538
+ labelMode: IndustryLabelMode;
539
+ /** Cars that spot here, derived from the drawn span length. */
540
+ cars: number;
541
+ carTypes: string[];
542
+ }
433
543
  interface ModuleFeatures {
434
544
  /** Whether either endplate declares a double-track main. */
435
545
  doubleMain: boolean;
@@ -454,6 +564,8 @@ interface ModuleFeatures {
454
564
  crossovers: DrawCrossover[];
455
565
  /** Branch endplates — junction connectors off the module (#170). */
456
566
  branchConnectors: BranchConnector[];
567
+ /** Rail-served industries — car-spot spans beside their track (#industries). */
568
+ industries: DrawIndustry[];
457
569
  /** Main 2's extent when it doesn't run the full module — a single↔double
458
570
  * transition (Main 2 starts/ends at the mainline turnout). Null = full
459
571
  * length (or no Main 2). Renderers draw the partial line + its diverge. */
@@ -559,4 +671,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
559
671
  heading: number;
560
672
  }>;
561
673
 
562
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleFeatures, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
674
+ export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, type IndustryLabelMode, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
package/dist/index.d.ts CHANGED
@@ -132,6 +132,35 @@ interface SchematicControlPoint {
132
132
  crossings?: string[];
133
133
  signals?: SchematicSignal[];
134
134
  }
135
+ /** What a rendered industry shows beside its name: a car count, a length in
136
+ * inches, or nothing (name only). Authored per industry. */
137
+ type IndustryLabelMode = "none" | "cars" | "inches";
138
+ /**
139
+ * An industry — a rail-served customer that spots cars, authored as a SPAN on a
140
+ * track (a spur/siding, or the main). Positional like everything else: it lives
141
+ * in the same module-local inch frame and is rendered into the shared 2-D view,
142
+ * offset to `side`. The span length gives its car capacity; the dispatcher and
143
+ * crews read where cars set out. Mirrors a `freemon_industries` row.
144
+ */
145
+ interface SchematicIndustry {
146
+ id: string;
147
+ name: string;
148
+ /** Industry type value from the lookup (e.g. "team_track", "grain"). */
149
+ type?: string | null;
150
+ /** The track this industry spots cars on (a spur/siding id, or the main). */
151
+ track: string;
152
+ /** The car-spot span along that track, inches from endplate A. */
153
+ fromPos: number;
154
+ toPos: number;
155
+ /** Which side of the track the building + label sit on. */
156
+ side?: SignalSide;
157
+ /** Secondary readout at the label — a car count, a length, or none. */
158
+ labelMode?: IndustryLabelMode;
159
+ /** Car types this industry receives (car-type value strings). */
160
+ carTypes?: string[];
161
+ /** The `freemon_industries` row this is (single source of truth); null = new. */
162
+ moduleIndustryId?: number | null;
163
+ }
135
164
  interface ModuleSchematicDoc {
136
165
  version: number;
137
166
  module?: string;
@@ -156,6 +185,8 @@ interface ModuleSchematicDoc {
156
185
  /** Grade crossings / diamonds (#170). */
157
186
  crossings?: SchematicCrossing[];
158
187
  controlPoints?: SchematicControlPoint[];
188
+ /** Rail-served industries — car-spot spans on a track (#industries). */
189
+ industries?: SchematicIndustry[];
159
190
  /** Benchwork FOOTPRINT outline — the module's physical board shape as a
160
191
  * polygon in module-local inches, in the same frame as the endplate poses
161
192
  * (endplate A's track point at the origin, the mainline along +x, perpendicular
@@ -191,6 +222,45 @@ declare function sampleBenchworkOutline(pts: BenchworkPoint[], segsPerArc?: numb
191
222
  x: number;
192
223
  y: number;
193
224
  }[];
225
+ interface ModuleFootprintInput {
226
+ /** Mainline length (falls back to footprint length), inches. */
227
+ lengthInches: number;
228
+ geometryType?: string | null;
229
+ geometryDegrees?: number | null;
230
+ geometryOffsetInches?: number | null;
231
+ /** Authored endplate face widths by id ("A"/"B"…), inches; default recommended. */
232
+ endplateWidths?: Record<string, number>;
233
+ /** Authored benchwork outline (module-local inches), or absent for the band. */
234
+ outline?: BenchworkPoint[] | null;
235
+ }
236
+ interface OutlineFace {
237
+ /** The endplate face's two corners + midpoint (the track point). */
238
+ p1: BenchworkPoint;
239
+ p2: BenchworkPoint;
240
+ mid: BenchworkPoint;
241
+ }
242
+ interface ModuleFootprint {
243
+ /** Main track centre-line A→B (arcs sampled). */
244
+ centerline: BenchworkPoint[];
245
+ /** Derived benchwork band (endplate-width ribbon); the outline fallback. */
246
+ band: BenchworkPoint[];
247
+ /** Endplate faces: [A end, B end]. */
248
+ endplateFaces: OutlineFace[];
249
+ /** Authored outline (arc-sampled closed ring) or null → render the band. */
250
+ outline: BenchworkPoint[] | null;
251
+ }
252
+ /** Module-local main track centre-line (A→B), sampling arcs for curves/corners. */
253
+ declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
254
+ /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
255
+ declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
256
+ /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
257
+ declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number): OutlineFace[];
258
+ /**
259
+ * The full single-module physical footprint: centre-line + derived band +
260
+ * endplate faces + the authored outline (arc-sampled), all in module-local
261
+ * inches. Renderers draw `outline ?? band`.
262
+ */
263
+ declare function moduleFootprint(input: ModuleFootprintInput): ModuleFootprint;
194
264
  /** Whether a doc is a single-endplate turnback (explicit flag or one endplate). */
195
265
  declare function isLoopDoc(doc: ModuleSchematicDoc): boolean;
196
266
  declare const MAIN_TRACK_ID = "main";
@@ -204,6 +274,13 @@ declare const N_SCALE_RATIO = 160;
204
274
  declare function inchesToScaleFeet(inches: number, ratio?: number): number;
205
275
  /** Scale feet of prototype track → real inches on the module. */
206
276
  declare function scaleFeetToInches(feet: number, ratio?: number): number;
277
+ /** Length a spotted car occupies on N-scale track, inches. A 40-ft car body is
278
+ * ~3.0″; ~3.3″ over the couplers — the real spacing a cut of cars takes. The
279
+ * single constant every repo reads so a track's car count matches everywhere. */
280
+ declare const N_CAR_LENGTH_INCHES = 3.3;
281
+ /** How many cars fit in a span, from its drawn length — the derived capacity a
282
+ * siding or an industry spot holds (never typed). */
283
+ declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
207
284
  /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
208
285
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
209
286
  interface EditorTrack {
@@ -255,6 +332,20 @@ interface EditorCrossing {
255
332
  trackA: string;
256
333
  trackB: string;
257
334
  }
335
+ /** An industry as the authoring form binds it — a car-spot span on a track. */
336
+ interface EditorIndustry {
337
+ id: string;
338
+ name: string;
339
+ type: string;
340
+ track: string;
341
+ fromPos: number;
342
+ toPos: number;
343
+ side: SignalSide;
344
+ labelMode: IndustryLabelMode;
345
+ carTypes: string[];
346
+ /** freemon_industries row (single source of truth), or null for a new one. */
347
+ moduleIndustryId: number | null;
348
+ }
258
349
  /** A 3rd+ endplate — a branch/junction connection off the module (#170).
259
350
  * A module may have several (e.g. a set carrying a second railroad through:
260
351
  * MoPac enters at one branch endplate and leaves at another). */
@@ -300,6 +391,8 @@ interface EditorState {
300
391
  * Empty = no authored outline (fall back to the endplate-width band). */
301
392
  outline: BenchworkPoint[];
302
393
  controlPoints: EditorControlPoint[];
394
+ /** Rail-served industries — car-spot spans on a track (#industries). */
395
+ industries: EditorIndustry[];
303
396
  }
304
397
  /** Build the empty editor state for a module of the given length. */
305
398
  declare function emptyEditorState(lengthInches: number): EditorState;
@@ -430,6 +523,23 @@ interface BranchConnector {
430
523
  posFrac: number;
431
524
  side: "up" | "down";
432
525
  }
526
+ /** An industry — draw a car-spot span beside its track's lane, on `side`, with
527
+ * a name label + an optional car/length readout (#industries). */
528
+ interface DrawIndustry {
529
+ id: string;
530
+ name: string;
531
+ type: string | null;
532
+ /** Span as fractions of module length (sorted West→East). */
533
+ fromFrac: number;
534
+ toFrac: number;
535
+ /** Lane of the track it spots on, so it draws beside the right track. */
536
+ lane: number;
537
+ side: SignalSide;
538
+ labelMode: IndustryLabelMode;
539
+ /** Cars that spot here, derived from the drawn span length. */
540
+ cars: number;
541
+ carTypes: string[];
542
+ }
433
543
  interface ModuleFeatures {
434
544
  /** Whether either endplate declares a double-track main. */
435
545
  doubleMain: boolean;
@@ -454,6 +564,8 @@ interface ModuleFeatures {
454
564
  crossovers: DrawCrossover[];
455
565
  /** Branch endplates — junction connectors off the module (#170). */
456
566
  branchConnectors: BranchConnector[];
567
+ /** Rail-served industries — car-spot spans beside their track (#industries). */
568
+ industries: DrawIndustry[];
457
569
  /** Main 2's extent when it doesn't run the full module — a single↔double
458
570
  * transition (Main 2 starts/ends at the mainline turnout). Null = full
459
571
  * length (or no Main 2). Renderers draw the partial line + its diverge. */
@@ -559,4 +671,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
559
671
  heading: number;
560
672
  }>;
561
673
 
562
- export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleFeatures, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
674
+ export { type BenchworkPoint, type BranchConnector, type DrawCrossing, type DrawCrossover, type DrawIndustry, type DrawSignal, type DrawTrack, type DrawTurnout, type EditorBranch, type EditorControlPoint, type EditorCpSignal, type EditorCrossing, type EditorIndustry, type EditorState, type EditorTrack, type EditorTurnout, type EndplateBConfig, type EndplatePose, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, type GeometryType, type IndustryLabelMode, MAIN2_TRACK_ID, MAIN_TRACK_ID, type ModuleFeatures, type ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, type OutlineFace, type SchematicBlock, type SchematicControlPoint, type SchematicCrossing, type SchematicEndplate, type SchematicEndplateTrack, type SchematicIndustry, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
package/dist/index.js CHANGED
@@ -63,6 +63,78 @@ function arcSweep(a0, a1, am) {
63
63
  const one = norm(a1);
64
64
  return m <= one ? one : one - 2 * Math.PI;
65
65
  }
66
+ var DEG_FP = Math.PI / 180;
67
+ function moduleCenterline(input) {
68
+ const L = input.lengthInches > 0 ? input.lengthInches : 24;
69
+ const gt = input.geometryType;
70
+ if (gt === "dead_end") return [{ x: 0, y: 0 }];
71
+ if (gt === "offset") return [{ x: 0, y: 0 }, { x: L, y: input.geometryOffsetInches ?? 0 }];
72
+ const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? input.geometryDegrees ?? 0 : 0;
73
+ if (turn === 0) return [{ x: 0, y: 0 }, { x: L, y: 0 }];
74
+ const t = turn * DEG_FP;
75
+ const r = L / t;
76
+ const steps = 12;
77
+ const pts = [];
78
+ for (let i = 0; i <= steps; i++) {
79
+ const a = t * i / steps;
80
+ pts.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
81
+ }
82
+ return pts;
83
+ }
84
+ function centerlineNormals(center) {
85
+ return center.map((_, i) => {
86
+ const a = center[Math.max(0, i - 1)];
87
+ const b = center[Math.min(center.length - 1, i + 1)];
88
+ let dx = b.x - a.x;
89
+ let dy = b.y - a.y;
90
+ const len = Math.hypot(dx, dy) || 1;
91
+ dx /= len;
92
+ dy /= len;
93
+ return { x: -dy, y: dx };
94
+ });
95
+ }
96
+ function centerlineFractions(center) {
97
+ const cum = [0];
98
+ for (let i = 1; i < center.length; i++)
99
+ cum.push(cum[i - 1] + Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y));
100
+ const total = cum[cum.length - 1] || 1;
101
+ return cum.map((d) => d / total);
102
+ }
103
+ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
104
+ if (center.length < 2) return [];
105
+ const n = centerlineNormals(center);
106
+ const f = centerlineFractions(center);
107
+ const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
108
+ const left = center.map((p, i) => ({ x: p.x + n[i].x * half(i), y: p.y + n[i].y * half(i) }));
109
+ const right = center.map((p, i) => ({ x: p.x - n[i].x * half(i), y: p.y - n[i].y * half(i) }));
110
+ return [...left, ...right.reverse()];
111
+ }
112
+ function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES) {
113
+ if (center.length < 2) return [];
114
+ const n = centerlineNormals(center);
115
+ const face = (i, w) => ({
116
+ p1: { x: center[i].x + n[i].x * (w / 2), y: center[i].y + n[i].y * (w / 2) },
117
+ p2: { x: center[i].x - n[i].x * (w / 2), y: center[i].y - n[i].y * (w / 2) },
118
+ mid: { x: center[i].x, y: center[i].y }
119
+ });
120
+ return [face(0, widthA), face(center.length - 1, widthB)];
121
+ }
122
+ function moduleFootprint(input) {
123
+ const centerline = moduleCenterline(input);
124
+ const widthA = endplateWidthFor(input.endplateWidths, "A");
125
+ const widthB = endplateWidthFor(input.endplateWidths, "B");
126
+ const authored = benchworkOutline(input);
127
+ return {
128
+ centerline,
129
+ band: benchworkBand(centerline, widthA, widthB),
130
+ endplateFaces: endplateFaceSegments(centerline, widthA, widthB),
131
+ outline: authored ? sampleBenchworkOutline(authored) : null
132
+ };
133
+ }
134
+ function endplateWidthFor(widths, id) {
135
+ const w = widths?.[id];
136
+ return typeof w === "number" && w > 0 ? w : FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES;
137
+ }
66
138
  function isLoopDoc(doc) {
67
139
  return doc.loop === true || doc.endplates.length === 1;
68
140
  }
@@ -75,6 +147,11 @@ function inchesToScaleFeet(inches, ratio = N_SCALE_RATIO) {
75
147
  function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
76
148
  return feet * 12 / ratio;
77
149
  }
150
+ var N_CAR_LENGTH_INCHES = 3.3;
151
+ function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
152
+ if (!(carLengthInches > 0)) return 0;
153
+ return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
154
+ }
78
155
  function asModuleSchematic(x) {
79
156
  if (!x || typeof x !== "object") return null;
80
157
  const d = x;
@@ -96,7 +173,8 @@ function emptyEditorState(lengthInches) {
96
173
  poseOverrides: {},
97
174
  endplateWidths: {},
98
175
  outline: [],
99
- controlPoints: []
176
+ controlPoints: [],
177
+ industries: []
100
178
  };
101
179
  }
102
180
  function isTransitionTurnout(t) {
@@ -266,6 +344,21 @@ function stateToDoc(state, recordNumber) {
266
344
  side: s.side
267
345
  }))
268
346
  })),
347
+ // Industries — car-spot spans on a track; only when any are authored.
348
+ ...state.industries.length > 0 ? {
349
+ industries: state.industries.map((ind) => ({
350
+ id: ind.id,
351
+ name: ind.name,
352
+ ...ind.type ? { type: ind.type } : {},
353
+ track: ind.track,
354
+ fromPos: ind.fromPos,
355
+ toPos: ind.toPos,
356
+ side: ind.side,
357
+ ...ind.labelMode && ind.labelMode !== "none" ? { labelMode: ind.labelMode } : {},
358
+ ...ind.carTypes.length ? { carTypes: ind.carTypes } : {},
359
+ moduleIndustryId: ind.moduleIndustryId
360
+ }))
361
+ } : {},
269
362
  // Benchwork footprint outline (module-local inches); only when it's a real
270
363
  // ring (≥ 3 vertices).
271
364
  ...state.outline.length >= 3 ? { outline: state.outline } : {}
@@ -379,7 +472,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
379
472
  divergeTrack: t.divergeTrack,
380
473
  kind: t.kind ?? "right"
381
474
  })),
382
- controlPoints: readControlPoints(d, sc)
475
+ controlPoints: readControlPoints(d, sc),
476
+ industries: (d.industries ?? []).map((ind) => ({
477
+ id: ind.id,
478
+ name: ind.name ?? "",
479
+ type: ind.type ?? "",
480
+ track: ind.track,
481
+ fromPos: sc(ind.fromPos ?? 0),
482
+ toPos: ind.toPos != null ? sc(ind.toPos) : len,
483
+ side: ind.side ?? "above",
484
+ labelMode: ind.labelMode ?? "none",
485
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : [],
486
+ moduleIndustryId: ind.moduleIndustryId ?? null
487
+ }))
383
488
  };
384
489
  }
385
490
  function readControlPoints(d, sc = (p) => p) {
@@ -678,6 +783,22 @@ function moduleFeatures(doc) {
678
783
  posFrac: clampFrac(e.at.pos),
679
784
  side: e.at.side === "down" ? "down" : "up"
680
785
  }));
786
+ const industries = (doc.industries ?? []).map((ind) => {
787
+ const from = ind.fromPos ?? 0;
788
+ const to = ind.toPos ?? len;
789
+ return {
790
+ id: ind.id,
791
+ name: ind.name ?? "",
792
+ type: ind.type ?? null,
793
+ fromFrac: clampFrac(Math.min(from, to)),
794
+ toFrac: clampFrac(Math.max(from, to)),
795
+ lane: trackLane.get(ind.track) ?? 0,
796
+ side: ind.side ?? "above",
797
+ labelMode: ind.labelMode ?? "none",
798
+ cars: carCapacity(from, to),
799
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : []
800
+ };
801
+ });
681
802
  const allLanes = [
682
803
  0,
683
804
  doubleMain ? 1 : 0,
@@ -718,6 +839,7 @@ function moduleFeatures(doc) {
718
839
  crossings,
719
840
  crossovers,
720
841
  branchConnectors,
842
+ industries,
721
843
  laneMin: Math.min(...allLanes),
722
844
  laneMax: Math.max(...allLanes)
723
845
  };
@@ -820,6 +942,6 @@ function poseOverridesFromDoc(doc) {
820
942
  return out;
821
943
  }
822
944
 
823
- export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_SCALE_RATIO, asModuleSchematic, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleFeatures, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
945
+ export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
824
946
  //# sourceMappingURL=index.js.map
825
947
  //# sourceMappingURL=index.js.map