@willcgage/module-schematic 0.17.0 → 0.19.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
@@ -78,6 +78,11 @@ interface SchematicTrack {
78
78
  * endplate A (past the throat = in the loop), lane is the ladder/arc index —
79
79
  * one record drives both the unrolled fan and a geometric render. */
80
80
  inLoop?: boolean;
81
+ /** Authored 2-D path for this track (module-local inches, open path with
82
+ * arcs) — the PHYSICAL shape a bent/rotated spur draws. Absent = derive from
83
+ * the main centre-line + lane, as before. Physical view only; the operations
84
+ * view stays positional (#2d-track). */
85
+ path?: BenchworkPoint[] | null;
81
86
  }
82
87
  interface SchematicTurnout {
83
88
  id: string;
@@ -132,6 +137,35 @@ interface SchematicControlPoint {
132
137
  crossings?: string[];
133
138
  signals?: SchematicSignal[];
134
139
  }
140
+ /** What a rendered industry shows beside its name: a car count, a length in
141
+ * inches, or nothing (name only). Authored per industry. */
142
+ type IndustryLabelMode = "none" | "cars" | "inches";
143
+ /**
144
+ * An industry — a rail-served customer that spots cars, authored as a SPAN on a
145
+ * track (a spur/siding, or the main). Positional like everything else: it lives
146
+ * in the same module-local inch frame and is rendered into the shared 2-D view,
147
+ * offset to `side`. The span length gives its car capacity; the dispatcher and
148
+ * crews read where cars set out. Mirrors a `freemon_industries` row.
149
+ */
150
+ interface SchematicIndustry {
151
+ id: string;
152
+ name: string;
153
+ /** Industry type value from the lookup (e.g. "team_track", "grain"). */
154
+ type?: string | null;
155
+ /** The track this industry spots cars on (a spur/siding id, or the main). */
156
+ track: string;
157
+ /** The car-spot span along that track, inches from endplate A. */
158
+ fromPos: number;
159
+ toPos: number;
160
+ /** Which side of the track the building + label sit on. */
161
+ side?: SignalSide;
162
+ /** Secondary readout at the label — a car count, a length, or none. */
163
+ labelMode?: IndustryLabelMode;
164
+ /** Car types this industry receives (car-type value strings). */
165
+ carTypes?: string[];
166
+ /** The `freemon_industries` row this is (single source of truth); null = new. */
167
+ moduleIndustryId?: number | null;
168
+ }
135
169
  interface ModuleSchematicDoc {
136
170
  version: number;
137
171
  module?: string;
@@ -156,6 +190,8 @@ interface ModuleSchematicDoc {
156
190
  /** Grade crossings / diamonds (#170). */
157
191
  crossings?: SchematicCrossing[];
158
192
  controlPoints?: SchematicControlPoint[];
193
+ /** Rail-served industries — car-spot spans on a track (#industries). */
194
+ industries?: SchematicIndustry[];
159
195
  /** Benchwork FOOTPRINT outline — the module's physical board shape as a
160
196
  * polygon in module-local inches, in the same frame as the endplate poses
161
197
  * (endplate A's track point at the origin, the mainline along +x, perpendicular
@@ -164,6 +200,10 @@ interface ModuleSchematicDoc {
164
200
  outline?: BenchworkPoint[];
165
201
  /** @deprecated pre-grouping flat signals; read for back-compat. */
166
202
  signals?: SchematicSignal[];
203
+ /** Authored mainline centre-line (module-local inches, open path with arcs).
204
+ * Present = the owner drew the real shape; absent = derive from geometry.
205
+ * Physical view only — the operations view stays derived (#2d-track). */
206
+ mainPath?: BenchworkPoint[] | null;
167
207
  }
168
208
  /** A benchwork-outline vertex, module-local inches. The edge from this vertex
169
209
  * to the NEXT one is a straight line, unless `bulge` is set — then it's a
@@ -191,6 +231,19 @@ declare function sampleBenchworkOutline(pts: BenchworkPoint[], segsPerArc?: numb
191
231
  x: number;
192
232
  y: number;
193
233
  }[];
234
+ /**
235
+ * Expand an OPEN track path (whose edges may be arcs) into a dense polyline —
236
+ * the open-ended sibling of sampleBenchworkOutline (which closes the ring).
237
+ * Used for authored track centre-lines (a drawn mainline or spur). The final
238
+ * vertex is always emitted so the path reaches its end.
239
+ */
240
+ declare function samplePath(pts: BenchworkPoint[], segsPerArc?: number): {
241
+ x: number;
242
+ y: number;
243
+ }[];
244
+ /** Normalise an authored track path from a doc, or null if it isn't a real path
245
+ * (needs ≥ 2 valid points). Keeps per-vertex bulge. */
246
+ declare function trackPath(path: BenchworkPoint[] | null | undefined): BenchworkPoint[] | null;
194
247
  interface ModuleFootprintInput {
195
248
  /** Mainline length (falls back to footprint length), inches. */
196
249
  lengthInches: number;
@@ -201,6 +254,10 @@ interface ModuleFootprintInput {
201
254
  endplateWidths?: Record<string, number>;
202
255
  /** Authored benchwork outline (module-local inches), or absent for the band. */
203
256
  outline?: BenchworkPoint[] | null;
257
+ /** Authored mainline centre-line (module-local inches, open path with arcs).
258
+ * When present it wins over the geometry-derived centre-line — the owner drew
259
+ * the real shape (#2d-track, physical view only). */
260
+ mainPath?: BenchworkPoint[] | null;
204
261
  }
205
262
  interface OutlineFace {
206
263
  /** The endplate face's two corners + midpoint (the track point). */
@@ -218,7 +275,9 @@ interface ModuleFootprint {
218
275
  /** Authored outline (arc-sampled closed ring) or null → render the band. */
219
276
  outline: BenchworkPoint[] | null;
220
277
  }
221
- /** Module-local main track centre-line (A→B), sampling arcs for curves/corners. */
278
+ /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
279
+ * An authored `mainPath` wins — the owner drew the real shape; otherwise the
280
+ * centre-line is derived from the geometry fields (length + type/degrees/offset). */
222
281
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
223
282
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
224
283
  declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
@@ -243,6 +302,13 @@ declare const N_SCALE_RATIO = 160;
243
302
  declare function inchesToScaleFeet(inches: number, ratio?: number): number;
244
303
  /** Scale feet of prototype track → real inches on the module. */
245
304
  declare function scaleFeetToInches(feet: number, ratio?: number): number;
305
+ /** Length a spotted car occupies on N-scale track, inches. A 40-ft car body is
306
+ * ~3.0″; ~3.3″ over the couplers — the real spacing a cut of cars takes. The
307
+ * single constant every repo reads so a track's car count matches everywhere. */
308
+ declare const N_CAR_LENGTH_INCHES = 3.3;
309
+ /** How many cars fit in a span, from its drawn length — the derived capacity a
310
+ * siding or an industry spot holds (never typed). */
311
+ declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
246
312
  /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
247
313
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
248
314
  interface EditorTrack {
@@ -257,6 +323,9 @@ interface EditorTrack {
257
323
  trackName: string;
258
324
  /** Inside the balloon of a loop module (#165). */
259
325
  inLoop?: boolean;
326
+ /** Authored 2-D path (module-local inches) — a bent/rotated spur's real
327
+ * shape. Absent = derive from the main + lane (#2d-track). */
328
+ path?: BenchworkPoint[];
260
329
  }
261
330
  /** A module_tracks row as loaded for the editor. */
262
331
  interface ModuleTrackRow {
@@ -294,6 +363,20 @@ interface EditorCrossing {
294
363
  trackA: string;
295
364
  trackB: string;
296
365
  }
366
+ /** An industry as the authoring form binds it — a car-spot span on a track. */
367
+ interface EditorIndustry {
368
+ id: string;
369
+ name: string;
370
+ type: string;
371
+ track: string;
372
+ fromPos: number;
373
+ toPos: number;
374
+ side: SignalSide;
375
+ labelMode: IndustryLabelMode;
376
+ carTypes: string[];
377
+ /** freemon_industries row (single source of truth), or null for a new one. */
378
+ moduleIndustryId: number | null;
379
+ }
297
380
  /** A 3rd+ endplate — a branch/junction connection off the module (#170).
298
381
  * A module may have several (e.g. a set carrying a second railroad through:
299
382
  * MoPac enters at one branch endplate and leaves at another). */
@@ -339,6 +422,11 @@ interface EditorState {
339
422
  * Empty = no authored outline (fall back to the endplate-width band). */
340
423
  outline: BenchworkPoint[];
341
424
  controlPoints: EditorControlPoint[];
425
+ /** Rail-served industries — car-spot spans on a track (#industries). */
426
+ industries: EditorIndustry[];
427
+ /** Authored mainline centre-line (module-local inches) — empty = derive from
428
+ * geometry. The owner-drawn real shape (#2d-track, physical view only). */
429
+ mainPath: BenchworkPoint[];
342
430
  }
343
431
  /** Build the empty editor state for a module of the given length. */
344
432
  declare function emptyEditorState(lengthInches: number): EditorState;
@@ -469,6 +557,23 @@ interface BranchConnector {
469
557
  posFrac: number;
470
558
  side: "up" | "down";
471
559
  }
560
+ /** An industry — draw a car-spot span beside its track's lane, on `side`, with
561
+ * a name label + an optional car/length readout (#industries). */
562
+ interface DrawIndustry {
563
+ id: string;
564
+ name: string;
565
+ type: string | null;
566
+ /** Span as fractions of module length (sorted West→East). */
567
+ fromFrac: number;
568
+ toFrac: number;
569
+ /** Lane of the track it spots on, so it draws beside the right track. */
570
+ lane: number;
571
+ side: SignalSide;
572
+ labelMode: IndustryLabelMode;
573
+ /** Cars that spot here, derived from the drawn span length. */
574
+ cars: number;
575
+ carTypes: string[];
576
+ }
472
577
  interface ModuleFeatures {
473
578
  /** Whether either endplate declares a double-track main. */
474
579
  doubleMain: boolean;
@@ -493,6 +598,8 @@ interface ModuleFeatures {
493
598
  crossovers: DrawCrossover[];
494
599
  /** Branch endplates — junction connectors off the module (#170). */
495
600
  branchConnectors: BranchConnector[];
601
+ /** Rail-served industries — car-spot spans beside their track (#industries). */
602
+ industries: DrawIndustry[];
496
603
  /** Main 2's extent when it doesn't run the full module — a single↔double
497
604
  * transition (Main 2 starts/ends at the mainline turnout). Null = full
498
605
  * length (or no Main 2). Renderers draw the partial line + its diverge. */
@@ -598,4 +705,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
598
705
  heading: number;
599
706
  }>;
600
707
 
601
- 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 ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type OutlineFace, 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, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
708
+ 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, samplePath, scaleFeetToInches, stateToDoc, trackPath };
package/dist/index.d.ts CHANGED
@@ -78,6 +78,11 @@ interface SchematicTrack {
78
78
  * endplate A (past the throat = in the loop), lane is the ladder/arc index —
79
79
  * one record drives both the unrolled fan and a geometric render. */
80
80
  inLoop?: boolean;
81
+ /** Authored 2-D path for this track (module-local inches, open path with
82
+ * arcs) — the PHYSICAL shape a bent/rotated spur draws. Absent = derive from
83
+ * the main centre-line + lane, as before. Physical view only; the operations
84
+ * view stays positional (#2d-track). */
85
+ path?: BenchworkPoint[] | null;
81
86
  }
82
87
  interface SchematicTurnout {
83
88
  id: string;
@@ -132,6 +137,35 @@ interface SchematicControlPoint {
132
137
  crossings?: string[];
133
138
  signals?: SchematicSignal[];
134
139
  }
140
+ /** What a rendered industry shows beside its name: a car count, a length in
141
+ * inches, or nothing (name only). Authored per industry. */
142
+ type IndustryLabelMode = "none" | "cars" | "inches";
143
+ /**
144
+ * An industry — a rail-served customer that spots cars, authored as a SPAN on a
145
+ * track (a spur/siding, or the main). Positional like everything else: it lives
146
+ * in the same module-local inch frame and is rendered into the shared 2-D view,
147
+ * offset to `side`. The span length gives its car capacity; the dispatcher and
148
+ * crews read where cars set out. Mirrors a `freemon_industries` row.
149
+ */
150
+ interface SchematicIndustry {
151
+ id: string;
152
+ name: string;
153
+ /** Industry type value from the lookup (e.g. "team_track", "grain"). */
154
+ type?: string | null;
155
+ /** The track this industry spots cars on (a spur/siding id, or the main). */
156
+ track: string;
157
+ /** The car-spot span along that track, inches from endplate A. */
158
+ fromPos: number;
159
+ toPos: number;
160
+ /** Which side of the track the building + label sit on. */
161
+ side?: SignalSide;
162
+ /** Secondary readout at the label — a car count, a length, or none. */
163
+ labelMode?: IndustryLabelMode;
164
+ /** Car types this industry receives (car-type value strings). */
165
+ carTypes?: string[];
166
+ /** The `freemon_industries` row this is (single source of truth); null = new. */
167
+ moduleIndustryId?: number | null;
168
+ }
135
169
  interface ModuleSchematicDoc {
136
170
  version: number;
137
171
  module?: string;
@@ -156,6 +190,8 @@ interface ModuleSchematicDoc {
156
190
  /** Grade crossings / diamonds (#170). */
157
191
  crossings?: SchematicCrossing[];
158
192
  controlPoints?: SchematicControlPoint[];
193
+ /** Rail-served industries — car-spot spans on a track (#industries). */
194
+ industries?: SchematicIndustry[];
159
195
  /** Benchwork FOOTPRINT outline — the module's physical board shape as a
160
196
  * polygon in module-local inches, in the same frame as the endplate poses
161
197
  * (endplate A's track point at the origin, the mainline along +x, perpendicular
@@ -164,6 +200,10 @@ interface ModuleSchematicDoc {
164
200
  outline?: BenchworkPoint[];
165
201
  /** @deprecated pre-grouping flat signals; read for back-compat. */
166
202
  signals?: SchematicSignal[];
203
+ /** Authored mainline centre-line (module-local inches, open path with arcs).
204
+ * Present = the owner drew the real shape; absent = derive from geometry.
205
+ * Physical view only — the operations view stays derived (#2d-track). */
206
+ mainPath?: BenchworkPoint[] | null;
167
207
  }
168
208
  /** A benchwork-outline vertex, module-local inches. The edge from this vertex
169
209
  * to the NEXT one is a straight line, unless `bulge` is set — then it's a
@@ -191,6 +231,19 @@ declare function sampleBenchworkOutline(pts: BenchworkPoint[], segsPerArc?: numb
191
231
  x: number;
192
232
  y: number;
193
233
  }[];
234
+ /**
235
+ * Expand an OPEN track path (whose edges may be arcs) into a dense polyline —
236
+ * the open-ended sibling of sampleBenchworkOutline (which closes the ring).
237
+ * Used for authored track centre-lines (a drawn mainline or spur). The final
238
+ * vertex is always emitted so the path reaches its end.
239
+ */
240
+ declare function samplePath(pts: BenchworkPoint[], segsPerArc?: number): {
241
+ x: number;
242
+ y: number;
243
+ }[];
244
+ /** Normalise an authored track path from a doc, or null if it isn't a real path
245
+ * (needs ≥ 2 valid points). Keeps per-vertex bulge. */
246
+ declare function trackPath(path: BenchworkPoint[] | null | undefined): BenchworkPoint[] | null;
194
247
  interface ModuleFootprintInput {
195
248
  /** Mainline length (falls back to footprint length), inches. */
196
249
  lengthInches: number;
@@ -201,6 +254,10 @@ interface ModuleFootprintInput {
201
254
  endplateWidths?: Record<string, number>;
202
255
  /** Authored benchwork outline (module-local inches), or absent for the band. */
203
256
  outline?: BenchworkPoint[] | null;
257
+ /** Authored mainline centre-line (module-local inches, open path with arcs).
258
+ * When present it wins over the geometry-derived centre-line — the owner drew
259
+ * the real shape (#2d-track, physical view only). */
260
+ mainPath?: BenchworkPoint[] | null;
204
261
  }
205
262
  interface OutlineFace {
206
263
  /** The endplate face's two corners + midpoint (the track point). */
@@ -218,7 +275,9 @@ interface ModuleFootprint {
218
275
  /** Authored outline (arc-sampled closed ring) or null → render the band. */
219
276
  outline: BenchworkPoint[] | null;
220
277
  }
221
- /** Module-local main track centre-line (A→B), sampling arcs for curves/corners. */
278
+ /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
279
+ * An authored `mainPath` wins — the owner drew the real shape; otherwise the
280
+ * centre-line is derived from the geometry fields (length + type/degrees/offset). */
222
281
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
223
282
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
224
283
  declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number): BenchworkPoint[];
@@ -243,6 +302,13 @@ declare const N_SCALE_RATIO = 160;
243
302
  declare function inchesToScaleFeet(inches: number, ratio?: number): number;
244
303
  /** Scale feet of prototype track → real inches on the module. */
245
304
  declare function scaleFeetToInches(feet: number, ratio?: number): number;
305
+ /** Length a spotted car occupies on N-scale track, inches. A 40-ft car body is
306
+ * ~3.0″; ~3.3″ over the couplers — the real spacing a cut of cars takes. The
307
+ * single constant every repo reads so a track's car count matches everywhere. */
308
+ declare const N_CAR_LENGTH_INCHES = 3.3;
309
+ /** How many cars fit in a span, from its drawn length — the derived capacity a
310
+ * siding or an industry spot holds (never typed). */
311
+ declare function carCapacity(fromPos: number, toPos: number, carLengthInches?: number): number;
246
312
  /** Parse a jsonb value into a schematic doc, or null if it isn't one. */
247
313
  declare function asModuleSchematic(x: unknown): ModuleSchematicDoc | null;
248
314
  interface EditorTrack {
@@ -257,6 +323,9 @@ interface EditorTrack {
257
323
  trackName: string;
258
324
  /** Inside the balloon of a loop module (#165). */
259
325
  inLoop?: boolean;
326
+ /** Authored 2-D path (module-local inches) — a bent/rotated spur's real
327
+ * shape. Absent = derive from the main + lane (#2d-track). */
328
+ path?: BenchworkPoint[];
260
329
  }
261
330
  /** A module_tracks row as loaded for the editor. */
262
331
  interface ModuleTrackRow {
@@ -294,6 +363,20 @@ interface EditorCrossing {
294
363
  trackA: string;
295
364
  trackB: string;
296
365
  }
366
+ /** An industry as the authoring form binds it — a car-spot span on a track. */
367
+ interface EditorIndustry {
368
+ id: string;
369
+ name: string;
370
+ type: string;
371
+ track: string;
372
+ fromPos: number;
373
+ toPos: number;
374
+ side: SignalSide;
375
+ labelMode: IndustryLabelMode;
376
+ carTypes: string[];
377
+ /** freemon_industries row (single source of truth), or null for a new one. */
378
+ moduleIndustryId: number | null;
379
+ }
297
380
  /** A 3rd+ endplate — a branch/junction connection off the module (#170).
298
381
  * A module may have several (e.g. a set carrying a second railroad through:
299
382
  * MoPac enters at one branch endplate and leaves at another). */
@@ -339,6 +422,11 @@ interface EditorState {
339
422
  * Empty = no authored outline (fall back to the endplate-width band). */
340
423
  outline: BenchworkPoint[];
341
424
  controlPoints: EditorControlPoint[];
425
+ /** Rail-served industries — car-spot spans on a track (#industries). */
426
+ industries: EditorIndustry[];
427
+ /** Authored mainline centre-line (module-local inches) — empty = derive from
428
+ * geometry. The owner-drawn real shape (#2d-track, physical view only). */
429
+ mainPath: BenchworkPoint[];
342
430
  }
343
431
  /** Build the empty editor state for a module of the given length. */
344
432
  declare function emptyEditorState(lengthInches: number): EditorState;
@@ -469,6 +557,23 @@ interface BranchConnector {
469
557
  posFrac: number;
470
558
  side: "up" | "down";
471
559
  }
560
+ /** An industry — draw a car-spot span beside its track's lane, on `side`, with
561
+ * a name label + an optional car/length readout (#industries). */
562
+ interface DrawIndustry {
563
+ id: string;
564
+ name: string;
565
+ type: string | null;
566
+ /** Span as fractions of module length (sorted West→East). */
567
+ fromFrac: number;
568
+ toFrac: number;
569
+ /** Lane of the track it spots on, so it draws beside the right track. */
570
+ lane: number;
571
+ side: SignalSide;
572
+ labelMode: IndustryLabelMode;
573
+ /** Cars that spot here, derived from the drawn span length. */
574
+ cars: number;
575
+ carTypes: string[];
576
+ }
472
577
  interface ModuleFeatures {
473
578
  /** Whether either endplate declares a double-track main. */
474
579
  doubleMain: boolean;
@@ -493,6 +598,8 @@ interface ModuleFeatures {
493
598
  crossovers: DrawCrossover[];
494
599
  /** Branch endplates — junction connectors off the module (#170). */
495
600
  branchConnectors: BranchConnector[];
601
+ /** Rail-served industries — car-spot spans beside their track (#industries). */
602
+ industries: DrawIndustry[];
496
603
  /** Main 2's extent when it doesn't run the full module — a single↔double
497
604
  * transition (Main 2 starts/ends at the mainline turnout). Null = full
498
605
  * length (or no Main 2). Renderers draw the partial line + its diverge. */
@@ -598,4 +705,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
598
705
  heading: number;
599
706
  }>;
600
707
 
601
- 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 ModuleFootprint, type ModuleFootprintInput, type ModuleGeometryInput, type ModuleSchematicDoc, type ModuleTrackRow, N_SCALE_RATIO, type OutlineFace, 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, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
708
+ 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, samplePath, scaleFeetToInches, stateToDoc, trackPath };
package/dist/index.js CHANGED
@@ -43,6 +43,45 @@ function sampleBenchworkOutline(pts, segsPerArc = 20) {
43
43
  }
44
44
  return out;
45
45
  }
46
+ function samplePath(pts, segsPerArc = 20) {
47
+ const n = pts.length;
48
+ if (n < 2) return pts.map((p) => ({ x: p.x, y: p.y }));
49
+ const out = [];
50
+ for (let i = 0; i < n - 1; i++) {
51
+ const p0 = pts[i];
52
+ const p1 = pts[i + 1];
53
+ out.push({ x: p0.x, y: p0.y });
54
+ const bulge = p0.bulge ?? 0;
55
+ if (!bulge) continue;
56
+ const dx = p1.x - p0.x;
57
+ const dy = p1.y - p0.y;
58
+ const c = Math.hypot(dx, dy);
59
+ if (c < 1e-6) continue;
60
+ const nx = -dy / c;
61
+ const ny = dx / c;
62
+ const mid = { x: (p0.x + p1.x) / 2 + nx * bulge, y: (p0.y + p1.y) / 2 + ny * bulge };
63
+ const circ = circleThrough(p0, mid, p1);
64
+ if (!circ) continue;
65
+ const a0 = Math.atan2(p0.y - circ.cy, p0.x - circ.cx);
66
+ const am = Math.atan2(mid.y - circ.cy, mid.x - circ.cx);
67
+ const a1 = Math.atan2(p1.y - circ.cy, p1.x - circ.cx);
68
+ const sweep = arcSweep(a0, a1, am);
69
+ for (let s = 1; s < segsPerArc; s++) {
70
+ const a = a0 + sweep * s / segsPerArc;
71
+ out.push({ x: circ.cx + circ.r * Math.cos(a), y: circ.cy + circ.r * Math.sin(a) });
72
+ }
73
+ }
74
+ out.push({ x: pts[n - 1].x, y: pts[n - 1].y });
75
+ return out;
76
+ }
77
+ function trackPath(path) {
78
+ const pts = (path ?? []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map((p) => ({
79
+ x: p.x,
80
+ y: p.y,
81
+ ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
82
+ }));
83
+ return pts.length >= 2 ? pts : null;
84
+ }
46
85
  function circleThrough(a, b, c) {
47
86
  const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
48
87
  if (Math.abs(d) < 1e-9) return null;
@@ -65,6 +104,8 @@ function arcSweep(a0, a1, am) {
65
104
  }
66
105
  var DEG_FP = Math.PI / 180;
67
106
  function moduleCenterline(input) {
107
+ const drawn = trackPath(input.mainPath);
108
+ if (drawn) return samplePath(drawn);
68
109
  const L = input.lengthInches > 0 ? input.lengthInches : 24;
69
110
  const gt = input.geometryType;
70
111
  if (gt === "dead_end") return [{ x: 0, y: 0 }];
@@ -147,6 +188,11 @@ function inchesToScaleFeet(inches, ratio = N_SCALE_RATIO) {
147
188
  function scaleFeetToInches(feet, ratio = N_SCALE_RATIO) {
148
189
  return feet * 12 / ratio;
149
190
  }
191
+ var N_CAR_LENGTH_INCHES = 3.3;
192
+ function carCapacity(fromPos, toPos, carLengthInches = N_CAR_LENGTH_INCHES) {
193
+ if (!(carLengthInches > 0)) return 0;
194
+ return Math.max(0, Math.floor(Math.abs(toPos - fromPos) / carLengthInches));
195
+ }
150
196
  function asModuleSchematic(x) {
151
197
  if (!x || typeof x !== "object") return null;
152
198
  const d = x;
@@ -168,7 +214,9 @@ function emptyEditorState(lengthInches) {
168
214
  poseOverrides: {},
169
215
  endplateWidths: {},
170
216
  outline: [],
171
- controlPoints: []
217
+ controlPoints: [],
218
+ industries: [],
219
+ mainPath: []
172
220
  };
173
221
  }
174
222
  function isTransitionTurnout(t) {
@@ -305,7 +353,8 @@ function stateToDoc(state, recordNumber) {
305
353
  moduleTrackId: t.moduleTrackId,
306
354
  trackName: t.trackName || void 0,
307
355
  capacityFeet: Math.round(inchesToScaleFeet(Math.abs(t.toPos - t.fromPos))),
308
- ...state.loop && t.inLoop ? { inLoop: true } : {}
356
+ ...state.loop && t.inLoop ? { inLoop: true } : {},
357
+ ...t.path && t.path.length >= 2 ? { path: t.path } : {}
309
358
  }))
310
359
  ],
311
360
  turnouts: state.turnouts.map((t) => ({
@@ -338,9 +387,26 @@ function stateToDoc(state, recordNumber) {
338
387
  side: s.side
339
388
  }))
340
389
  })),
390
+ // Industries — car-spot spans on a track; only when any are authored.
391
+ ...state.industries.length > 0 ? {
392
+ industries: state.industries.map((ind) => ({
393
+ id: ind.id,
394
+ name: ind.name,
395
+ ...ind.type ? { type: ind.type } : {},
396
+ track: ind.track,
397
+ fromPos: ind.fromPos,
398
+ toPos: ind.toPos,
399
+ side: ind.side,
400
+ ...ind.labelMode && ind.labelMode !== "none" ? { labelMode: ind.labelMode } : {},
401
+ ...ind.carTypes.length ? { carTypes: ind.carTypes } : {},
402
+ moduleIndustryId: ind.moduleIndustryId
403
+ }))
404
+ } : {},
341
405
  // Benchwork footprint outline (module-local inches); only when it's a real
342
406
  // ring (≥ 3 vertices).
343
- ...state.outline.length >= 3 ? { outline: state.outline } : {}
407
+ ...state.outline.length >= 3 ? { outline: state.outline } : {},
408
+ // Authored mainline path (module-local inches); only when it's a real path.
409
+ ...state.mainPath.length >= 2 ? { mainPath: state.mainPath } : {}
344
410
  };
345
411
  }
346
412
  function docToState(doc, fallbackLength, moduleTracks = []) {
@@ -370,7 +436,9 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
370
436
  toPos: t.toPos != null ? sc(t.toPos) : len,
371
437
  moduleTrackId,
372
438
  trackName: t.trackName ?? nameOf(moduleTrackId),
373
- ...t.inLoop ? { inLoop: true } : {}
439
+ ...t.inLoop ? { inLoop: true } : {},
440
+ // Authored path kept as-drawn (a physical shape, not rescaled with length).
441
+ ...trackPath(t.path) ? { path: trackPath(t.path) } : {}
374
442
  });
375
443
  }
376
444
  }
@@ -419,6 +487,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
419
487
  y: p.y,
420
488
  ...Number.isFinite(p.bulge) && p.bulge ? { bulge: p.bulge } : {}
421
489
  }));
490
+ const mainPath = trackPath(d.mainPath) ?? [];
422
491
  return {
423
492
  lengthInches: len,
424
493
  loop,
@@ -435,6 +504,7 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
435
504
  poseOverrides,
436
505
  endplateWidths,
437
506
  outline,
507
+ mainPath,
438
508
  crossings: (d.crossings ?? []).map((x) => ({
439
509
  id: x.id,
440
510
  name: x.name ?? "",
@@ -451,7 +521,19 @@ function docToState(doc, fallbackLength, moduleTracks = []) {
451
521
  divergeTrack: t.divergeTrack,
452
522
  kind: t.kind ?? "right"
453
523
  })),
454
- controlPoints: readControlPoints(d, sc)
524
+ controlPoints: readControlPoints(d, sc),
525
+ industries: (d.industries ?? []).map((ind) => ({
526
+ id: ind.id,
527
+ name: ind.name ?? "",
528
+ type: ind.type ?? "",
529
+ track: ind.track,
530
+ fromPos: sc(ind.fromPos ?? 0),
531
+ toPos: ind.toPos != null ? sc(ind.toPos) : len,
532
+ side: ind.side ?? "above",
533
+ labelMode: ind.labelMode ?? "none",
534
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : [],
535
+ moduleIndustryId: ind.moduleIndustryId ?? null
536
+ }))
455
537
  };
456
538
  }
457
539
  function readControlPoints(d, sc = (p) => p) {
@@ -750,6 +832,22 @@ function moduleFeatures(doc) {
750
832
  posFrac: clampFrac(e.at.pos),
751
833
  side: e.at.side === "down" ? "down" : "up"
752
834
  }));
835
+ const industries = (doc.industries ?? []).map((ind) => {
836
+ const from = ind.fromPos ?? 0;
837
+ const to = ind.toPos ?? len;
838
+ return {
839
+ id: ind.id,
840
+ name: ind.name ?? "",
841
+ type: ind.type ?? null,
842
+ fromFrac: clampFrac(Math.min(from, to)),
843
+ toFrac: clampFrac(Math.max(from, to)),
844
+ lane: trackLane.get(ind.track) ?? 0,
845
+ side: ind.side ?? "above",
846
+ labelMode: ind.labelMode ?? "none",
847
+ cars: carCapacity(from, to),
848
+ carTypes: Array.isArray(ind.carTypes) ? ind.carTypes : []
849
+ };
850
+ });
753
851
  const allLanes = [
754
852
  0,
755
853
  doubleMain ? 1 : 0,
@@ -790,6 +888,7 @@ function moduleFeatures(doc) {
790
888
  crossings,
791
889
  crossovers,
792
890
  branchConnectors,
891
+ industries,
793
892
  laneMin: Math.min(...allLanes),
794
893
  laneMax: Math.max(...allLanes)
795
894
  };
@@ -892,6 +991,6 @@ function poseOverridesFromDoc(doc) {
892
991
  return out;
893
992
  }
894
993
 
895
- export { FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, scaleFeetToInches, stateToDoc };
994
+ 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, samplePath, scaleFeetToInches, stateToDoc, trackPath };
896
995
  //# sourceMappingURL=index.js.map
897
996
  //# sourceMappingURL=index.js.map