@willcgage/module-schematic 0.29.0 → 0.31.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
@@ -273,6 +273,18 @@ interface BenchworkPoint {
273
273
  bulge?: number;
274
274
  }
275
275
  /** One bench-work section of a module (#96 phase 2). */
276
+ interface SectionFootprint {
277
+ id: string;
278
+ name?: string;
279
+ outline: {
280
+ x: number;
281
+ y: number;
282
+ }[];
283
+ /** True when this shape is DERIVED from the section's span rather than
284
+ * authored — a derived outline follows the board when it's resized, an
285
+ * authored one stays exactly as drawn (#96 phase 2b). */
286
+ derived: boolean;
287
+ }
276
288
  interface SchematicSection {
277
289
  id: string;
278
290
  /** What the owner calls this board — "west transition", "double #3". */
@@ -281,6 +293,19 @@ interface SchematicSection {
281
293
  * the module outline. Absent = the section has no shape of its own yet (it's
282
294
  * described only by the joints in `sectionBreaks`). */
283
295
  outline?: BenchworkPoint[] | null;
296
+ /** How far this board runs along the main, inches. The module's length is the
297
+ * SUM of these — it isn't authored separately (#108). */
298
+ lengthInches?: number | null;
299
+ /** This board's own shape: straight | curve | corner_45 | corner_90 | offset |
300
+ * dead_end. Geometry belongs to the SECTION, not the module — a module like
301
+ * One Mile is 384″ of mostly-straight boards with two 24″ CURVED sections in
302
+ * the middle, which no single module-level geometry can describe (#108).
303
+ * Absent = straight. */
304
+ geometryType?: string | null;
305
+ /** Degrees turned, for curve/corner sections. */
306
+ geometryDegrees?: number | null;
307
+ /** Lateral jog, for offset sections. */
308
+ geometryOffsetInches?: number | null;
284
309
  }
285
310
  /** The authored benchwork outline, or null when a module hasn't drawn one
286
311
  * (renderers then fall back to a band derived from the endplate widths). A
@@ -301,14 +326,18 @@ declare function moduleSections(doc: {
301
326
  * bring in real clipping. */
302
327
  declare function sectionFootprints(doc: {
303
328
  sections?: SchematicSection[] | null;
304
- } | null | undefined): {
305
- id: string;
306
- name?: string;
307
- outline: {
308
- x: number;
309
- y: number;
310
- }[];
311
- }[];
329
+ } | null | undefined,
330
+ /** The module's spine and dimensions. Given these, a section with no
331
+ * authored polygon gets a band derived from its own span, so every section
332
+ * has a shape and a resized board reshapes with it (#96 phase 2b). Omit to
333
+ * get authored outlines only. */
334
+ derive?: {
335
+ centerline: BenchworkPoint[];
336
+ widthA: number;
337
+ widthB: number;
338
+ offsetA: number;
339
+ offsetB: number;
340
+ }): SectionFootprint[];
312
341
  /**
313
342
  * Expand a benchwork outline (whose edges may be arcs) into a dense closed
314
343
  * polyline for rendering — the SAME sampling both the Repository preview and
@@ -374,24 +403,58 @@ interface ModuleFootprint {
374
403
  /** Authored outline (arc-sampled closed ring) or null → render the band.
375
404
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
376
405
  outline: BenchworkPoint[] | null;
377
- /** Per-section footprints, arc-sampled (#96 phase 2). Draw every one: together
406
+ /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
378
407
  * they are the module's footprint. Empty = this module doesn't use sections,
379
408
  * so fall back to `outline ?? band` exactly as before. */
380
- sectionOutlines: {
381
- id: string;
382
- name?: string;
383
- outline: {
384
- x: number;
385
- y: number;
386
- }[];
387
- }[];
409
+ sectionOutlines: SectionFootprint[];
388
410
  }
389
411
  /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
390
412
  * An authored `mainPath` wins — the owner drew the real shape; otherwise the
391
413
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
392
414
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
415
+ /** The module's length as the SUM of its sections, or null when it has none
416
+ * (then the authored module length still speaks). */
417
+ declare function moduleLengthFromSections(doc: {
418
+ sections?: SchematicSection[] | null;
419
+ } | null | undefined): number | null;
420
+ /** Where each section starts and ends along the main, inches from endplate A.
421
+ * This is what `sectionBreaks` used to author by hand — now derived, so a
422
+ * length is just a number you type and nothing steals from its neighbour. */
423
+ declare function sectionSpans(doc: {
424
+ sections?: SchematicSection[] | null;
425
+ } | null | undefined): {
426
+ id: string;
427
+ name?: string;
428
+ fromPos: number;
429
+ toPos: number;
430
+ }[];
431
+ /** The joints implied by the sections — the interior boundaries, in inches from
432
+ * endplate A. Replaces the authored `sectionBreaks` for a sectioned module. */
433
+ declare function sectionBreaksFromSections(doc: {
434
+ sections?: SchematicSection[] | null;
435
+ } | null | undefined): number[];
436
+ /** The module's centre-line built by CHAINING its sections — each board starts
437
+ * where the previous one ended, at the heading it ended on. This is what makes
438
+ * a module like One Mile expressible: straight boards with two 24″ curved ones
439
+ * in the middle, which no single module-level geometry can describe (#108).
440
+ * Returns [] when the module has no sections with lengths. */
441
+ declare function sectionedCenterline(doc: {
442
+ sections?: SchematicSection[] | null;
443
+ } | null | undefined): BenchworkPoint[];
393
444
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
394
445
  declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
446
+ /** The sub-polyline of a centre-line between two arc-length positions, with
447
+ * the cut ends interpolated so a slice starts and finishes exactly on them. */
448
+ declare function sliceCenterline(center: BenchworkPoint[], fromPos: number, toPos: number): BenchworkPoint[];
449
+ /** One section's bench-work as a band over its own stretch of centre-line —
450
+ * the per-section equivalent of `benchworkBand` (#96 phase 2b). Width and
451
+ * plate offset are interpolated from the module's ends exactly as the whole
452
+ * band does, so a section's derived shape lines up with its neighbours.
453
+ *
454
+ * This is what makes an outline BELONG to the section: a section without an
455
+ * authored polygon gets one derived from its span, so resizing the board
456
+ * reshapes it instead of leaving a hand-drawn outline stranded. */
457
+ declare function sectionBand(center: BenchworkPoint[], fromPos: number, toPos: number, widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
395
458
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
396
459
  declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
397
460
  /**
@@ -878,4 +941,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
878
941
  heading: number;
879
942
  }>;
880
943
 
881
- 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, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, 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 SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionFootprints, stateToDoc, trackPath };
944
+ 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, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, 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 SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionFootprint, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionBand, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, sliceCenterline, stateToDoc, trackPath };
package/dist/index.d.ts CHANGED
@@ -273,6 +273,18 @@ interface BenchworkPoint {
273
273
  bulge?: number;
274
274
  }
275
275
  /** One bench-work section of a module (#96 phase 2). */
276
+ interface SectionFootprint {
277
+ id: string;
278
+ name?: string;
279
+ outline: {
280
+ x: number;
281
+ y: number;
282
+ }[];
283
+ /** True when this shape is DERIVED from the section's span rather than
284
+ * authored — a derived outline follows the board when it's resized, an
285
+ * authored one stays exactly as drawn (#96 phase 2b). */
286
+ derived: boolean;
287
+ }
276
288
  interface SchematicSection {
277
289
  id: string;
278
290
  /** What the owner calls this board — "west transition", "double #3". */
@@ -281,6 +293,19 @@ interface SchematicSection {
281
293
  * the module outline. Absent = the section has no shape of its own yet (it's
282
294
  * described only by the joints in `sectionBreaks`). */
283
295
  outline?: BenchworkPoint[] | null;
296
+ /** How far this board runs along the main, inches. The module's length is the
297
+ * SUM of these — it isn't authored separately (#108). */
298
+ lengthInches?: number | null;
299
+ /** This board's own shape: straight | curve | corner_45 | corner_90 | offset |
300
+ * dead_end. Geometry belongs to the SECTION, not the module — a module like
301
+ * One Mile is 384″ of mostly-straight boards with two 24″ CURVED sections in
302
+ * the middle, which no single module-level geometry can describe (#108).
303
+ * Absent = straight. */
304
+ geometryType?: string | null;
305
+ /** Degrees turned, for curve/corner sections. */
306
+ geometryDegrees?: number | null;
307
+ /** Lateral jog, for offset sections. */
308
+ geometryOffsetInches?: number | null;
284
309
  }
285
310
  /** The authored benchwork outline, or null when a module hasn't drawn one
286
311
  * (renderers then fall back to a band derived from the endplate widths). A
@@ -301,14 +326,18 @@ declare function moduleSections(doc: {
301
326
  * bring in real clipping. */
302
327
  declare function sectionFootprints(doc: {
303
328
  sections?: SchematicSection[] | null;
304
- } | null | undefined): {
305
- id: string;
306
- name?: string;
307
- outline: {
308
- x: number;
309
- y: number;
310
- }[];
311
- }[];
329
+ } | null | undefined,
330
+ /** The module's spine and dimensions. Given these, a section with no
331
+ * authored polygon gets a band derived from its own span, so every section
332
+ * has a shape and a resized board reshapes with it (#96 phase 2b). Omit to
333
+ * get authored outlines only. */
334
+ derive?: {
335
+ centerline: BenchworkPoint[];
336
+ widthA: number;
337
+ widthB: number;
338
+ offsetA: number;
339
+ offsetB: number;
340
+ }): SectionFootprint[];
312
341
  /**
313
342
  * Expand a benchwork outline (whose edges may be arcs) into a dense closed
314
343
  * polyline for rendering — the SAME sampling both the Repository preview and
@@ -374,24 +403,58 @@ interface ModuleFootprint {
374
403
  /** Authored outline (arc-sampled closed ring) or null → render the band.
375
404
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
376
405
  outline: BenchworkPoint[] | null;
377
- /** Per-section footprints, arc-sampled (#96 phase 2). Draw every one: together
406
+ /** Per-section footprints, arc-sampled (#96 phase 2b). Draw every one: together
378
407
  * they are the module's footprint. Empty = this module doesn't use sections,
379
408
  * so fall back to `outline ?? band` exactly as before. */
380
- sectionOutlines: {
381
- id: string;
382
- name?: string;
383
- outline: {
384
- x: number;
385
- y: number;
386
- }[];
387
- }[];
409
+ sectionOutlines: SectionFootprint[];
388
410
  }
389
411
  /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
390
412
  * An authored `mainPath` wins — the owner drew the real shape; otherwise the
391
413
  * centre-line is derived from the geometry fields (length + type/degrees/offset). */
392
414
  declare function moduleCenterline(input: ModuleFootprintInput): BenchworkPoint[];
415
+ /** The module's length as the SUM of its sections, or null when it has none
416
+ * (then the authored module length still speaks). */
417
+ declare function moduleLengthFromSections(doc: {
418
+ sections?: SchematicSection[] | null;
419
+ } | null | undefined): number | null;
420
+ /** Where each section starts and ends along the main, inches from endplate A.
421
+ * This is what `sectionBreaks` used to author by hand — now derived, so a
422
+ * length is just a number you type and nothing steals from its neighbour. */
423
+ declare function sectionSpans(doc: {
424
+ sections?: SchematicSection[] | null;
425
+ } | null | undefined): {
426
+ id: string;
427
+ name?: string;
428
+ fromPos: number;
429
+ toPos: number;
430
+ }[];
431
+ /** The joints implied by the sections — the interior boundaries, in inches from
432
+ * endplate A. Replaces the authored `sectionBreaks` for a sectioned module. */
433
+ declare function sectionBreaksFromSections(doc: {
434
+ sections?: SchematicSection[] | null;
435
+ } | null | undefined): number[];
436
+ /** The module's centre-line built by CHAINING its sections — each board starts
437
+ * where the previous one ended, at the heading it ended on. This is what makes
438
+ * a module like One Mile expressible: straight boards with two 24″ curved ones
439
+ * in the middle, which no single module-level geometry can describe (#108).
440
+ * Returns [] when the module has no sections with lengths. */
441
+ declare function sectionedCenterline(doc: {
442
+ sections?: SchematicSection[] | null;
443
+ } | null | undefined): BenchworkPoint[];
393
444
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
394
445
  declare function benchworkBand(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
446
+ /** The sub-polyline of a centre-line between two arc-length positions, with
447
+ * the cut ends interpolated so a slice starts and finishes exactly on them. */
448
+ declare function sliceCenterline(center: BenchworkPoint[], fromPos: number, toPos: number): BenchworkPoint[];
449
+ /** One section's bench-work as a band over its own stretch of centre-line —
450
+ * the per-section equivalent of `benchworkBand` (#96 phase 2b). Width and
451
+ * plate offset are interpolated from the module's ends exactly as the whole
452
+ * band does, so a section's derived shape lines up with its neighbours.
453
+ *
454
+ * This is what makes an outline BELONG to the section: a section without an
455
+ * authored polygon gets one derived from its span, so resizing the board
456
+ * reshapes it instead of leaving a hand-drawn outline stranded. */
457
+ declare function sectionBand(center: BenchworkPoint[], fromPos: number, toPos: number, widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): BenchworkPoint[];
395
458
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
396
459
  declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
397
460
  /**
@@ -878,4 +941,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
878
941
  heading: number;
879
942
  }>;
880
943
 
881
- 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, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, 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 SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionFootprints, stateToDoc, trackPath };
944
+ 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, type EndplateWidthIssue, FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, type GeometryType, type IndustryLabelMode, type IndustrySpot, 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 SchematicSection, type SchematicSignal, type SchematicTrack, type SchematicTurnout, type SectionFootprint, type SignalFacing, type SignalSide, type TrackConfig, type TrackRole, type TurnoutKind, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionBand, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, sliceCenterline, stateToDoc, trackPath };
package/dist/index.js CHANGED
@@ -19,19 +19,40 @@ function moduleSections(doc) {
19
19
  return (doc?.sections ?? []).filter((sec) => sec && typeof sec.id === "string" && sec.id !== "").map((sec) => {
20
20
  const outline = benchworkOutline({ outline: sec.outline });
21
21
  const name = typeof sec.name === "string" ? sec.name.trim() : "";
22
+ const len = sec.lengthInches;
23
+ const deg = sec.geometryDegrees;
24
+ const off = sec.geometryOffsetInches;
22
25
  return {
23
26
  id: sec.id,
24
27
  ...name ? { name } : {},
25
- ...outline ? { outline } : {}
28
+ ...outline ? { outline } : {},
29
+ ...typeof len === "number" && Number.isFinite(len) && len > 0 ? { lengthInches: len } : {},
30
+ ...sec.geometryType ? { geometryType: sec.geometryType } : {},
31
+ ...typeof deg === "number" && Number.isFinite(deg) ? { geometryDegrees: deg } : {},
32
+ ...typeof off === "number" && Number.isFinite(off) ? { geometryOffsetInches: off } : {}
26
33
  };
27
34
  });
28
35
  }
29
- function sectionFootprints(doc) {
30
- return moduleSections(doc).filter((sec) => sec.outline).map((sec) => ({
31
- id: sec.id,
32
- ...sec.name ? { name: sec.name } : {},
33
- outline: sampleBenchworkOutline(sec.outline)
34
- }));
36
+ function sectionFootprints(doc, derive) {
37
+ const spans = derive ? sectionSpans(doc) : [];
38
+ const spanOf = new Map(spans.map((sp) => [sp.id, sp]));
39
+ return moduleSections(doc).map((sec) => {
40
+ const name = sec.name ? { name: sec.name } : {};
41
+ if (sec.outline)
42
+ return { id: sec.id, ...name, outline: sampleBenchworkOutline(sec.outline), derived: false };
43
+ const sp = spanOf.get(sec.id);
44
+ if (!sp || !derive) return null;
45
+ const band = sectionBand(
46
+ derive.centerline,
47
+ sp.fromPos,
48
+ sp.toPos,
49
+ derive.widthA,
50
+ derive.widthB,
51
+ derive.offsetA,
52
+ derive.offsetB
53
+ );
54
+ return band.length >= 3 ? { id: sec.id, ...name, outline: band, derived: true } : null;
55
+ }).filter((x) => x !== null);
35
56
  }
36
57
  function sampleBenchworkOutline(pts, segsPerArc = 20) {
37
58
  const n = pts.length;
@@ -126,6 +147,8 @@ var DEG_FP = Math.PI / 180;
126
147
  function moduleCenterline(input) {
127
148
  const drawn = trackPath(input.mainPath);
128
149
  if (drawn) return samplePath(drawn);
150
+ const chained = sectionedCenterline(input);
151
+ if (chained.length >= 2) return chained;
129
152
  if (!input.geometryType) return [];
130
153
  const L = input.lengthInches > 0 ? input.lengthInches : 24;
131
154
  const gt = input.geometryType;
@@ -143,6 +166,73 @@ function moduleCenterline(input) {
143
166
  }
144
167
  return pts;
145
168
  }
169
+ function sectionCenterlineLocal(sec) {
170
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
171
+ const gt = sec.geometryType || "straight";
172
+ if (L <= 0) return { points: [{ x: 0, y: 0 }], endX: 0, endY: 0, endHeadingDeg: 0 };
173
+ if (gt === "offset") {
174
+ const dy = sec.geometryOffsetInches ?? 0;
175
+ return { points: [{ x: 0, y: 0 }, { x: L, y: dy }], endX: L, endY: dy, endHeadingDeg: 0 };
176
+ }
177
+ const turn = gt === "corner_45" ? 45 : gt === "corner_90" ? 90 : gt === "curve" ? sec.geometryDegrees ?? 0 : 0;
178
+ if (turn === 0) return { points: [{ x: 0, y: 0 }, { x: L, y: 0 }], endX: L, endY: 0, endHeadingDeg: 0 };
179
+ const t = turn * DEG_FP;
180
+ const r = L / t;
181
+ const steps = 12;
182
+ const points = [];
183
+ for (let i = 0; i <= steps; i++) {
184
+ const a = t * i / steps;
185
+ points.push({ x: r * Math.sin(a), y: r * (1 - Math.cos(a)) });
186
+ }
187
+ const last = points[points.length - 1];
188
+ return { points, endX: last.x, endY: last.y, endHeadingDeg: turn };
189
+ }
190
+ function moduleLengthFromSections(doc) {
191
+ const secs = moduleSections(doc).filter(
192
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
193
+ );
194
+ if (!secs.length) return null;
195
+ return secs.reduce((a, sec) => a + sec.lengthInches, 0);
196
+ }
197
+ function sectionSpans(doc) {
198
+ let acc = 0;
199
+ const out = [];
200
+ for (const sec of moduleSections(doc)) {
201
+ const L = typeof sec.lengthInches === "number" && sec.lengthInches > 0 ? sec.lengthInches : 0;
202
+ if (L <= 0) continue;
203
+ out.push({ id: sec.id, ...sec.name ? { name: sec.name } : {}, fromPos: acc, toPos: acc + L });
204
+ acc += L;
205
+ }
206
+ return out;
207
+ }
208
+ function sectionBreaksFromSections(doc) {
209
+ const spans = sectionSpans(doc);
210
+ return spans.slice(0, -1).map((sp) => sp.toPos);
211
+ }
212
+ function sectionedCenterline(doc) {
213
+ const secs = moduleSections(doc).filter(
214
+ (sec) => typeof sec.lengthInches === "number" && sec.lengthInches > 0
215
+ );
216
+ if (!secs.length) return [];
217
+ const out = [];
218
+ let ox = 0;
219
+ let oy = 0;
220
+ let heading = 0;
221
+ for (const sec of secs) {
222
+ const local = sectionCenterlineLocal(sec);
223
+ const c = Math.cos(heading * DEG_FP);
224
+ const sn = Math.sin(heading * DEG_FP);
225
+ for (let i = 0; i < local.points.length; i++) {
226
+ if (i === 0 && out.length) continue;
227
+ const p = local.points[i];
228
+ out.push({ x: ox + p.x * c - p.y * sn, y: oy + p.x * sn + p.y * c });
229
+ }
230
+ ox += local.endX * c - local.endY * sn;
231
+ oy += local.endX * sn + local.endY * c;
232
+ heading += local.endHeadingDeg;
233
+ }
234
+ return out;
235
+ }
146
236
  function centerlineNormals(center) {
147
237
  return center.map((_, i) => {
148
238
  const a = center[Math.max(0, i - 1)];
@@ -178,6 +268,63 @@ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES
178
268
  }));
179
269
  return [...left, ...right.reverse()];
180
270
  }
271
+ function sliceCenterline(center, fromPos, toPos) {
272
+ if (center.length < 2) return [];
273
+ const cum = [0];
274
+ for (let i = 1; i < center.length; i++)
275
+ cum.push(cum[i - 1] + Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y));
276
+ const total = cum[cum.length - 1];
277
+ const a = Math.max(0, Math.min(total, Math.min(fromPos, toPos)));
278
+ const b = Math.max(0, Math.min(total, Math.max(fromPos, toPos)));
279
+ if (b - a <= 0) return [];
280
+ const at = (d) => {
281
+ for (let i = 1; i < center.length; i++) {
282
+ if (d <= cum[i] || i === center.length - 1) {
283
+ const seg = cum[i] - cum[i - 1] || 1;
284
+ const t = Math.max(0, Math.min(1, (d - cum[i - 1]) / seg));
285
+ return {
286
+ x: center[i - 1].x + (center[i].x - center[i - 1].x) * t,
287
+ y: center[i - 1].y + (center[i].y - center[i - 1].y) * t
288
+ };
289
+ }
290
+ }
291
+ return center[center.length - 1];
292
+ };
293
+ const out = [at(a)];
294
+ for (let i = 0; i < center.length; i++) {
295
+ if (cum[i] > a && cum[i] < b) out.push({ x: center[i].x, y: center[i].y });
296
+ }
297
+ out.push(at(b));
298
+ return out;
299
+ }
300
+ function sectionBand(center, fromPos, toPos, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
301
+ const slice = sliceCenterline(center, fromPos, toPos);
302
+ if (slice.length < 2) return [];
303
+ let total = 0;
304
+ for (let i = 1; i < center.length; i++)
305
+ total += Math.hypot(center[i].x - center[i - 1].x, center[i].y - center[i - 1].y);
306
+ total = total || 1;
307
+ const lo = Math.min(fromPos, toPos);
308
+ let acc = 0;
309
+ const fr = [0];
310
+ for (let i = 1; i < slice.length; i++) {
311
+ acc += Math.hypot(slice[i].x - slice[i - 1].x, slice[i].y - slice[i - 1].y);
312
+ fr.push(acc);
313
+ }
314
+ const f = fr.map((d) => Math.max(0, Math.min(1, (lo + d) / total)));
315
+ const n = centerlineNormals(slice);
316
+ const half = (i) => (widthA * (1 - f[i]) + widthB * f[i]) / 2;
317
+ const off = (i) => offsetA * (1 - f[i]) + offsetB * f[i];
318
+ const left = slice.map((p, i) => ({
319
+ x: p.x + n[i].x * (off(i) + half(i)),
320
+ y: p.y + n[i].y * (off(i) + half(i))
321
+ }));
322
+ const right = slice.map((p, i) => ({
323
+ x: p.x + n[i].x * (off(i) - half(i)),
324
+ y: p.y + n[i].y * (off(i) - half(i))
325
+ }));
326
+ return [...left, ...right.reverse()];
327
+ }
181
328
  function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
182
329
  if (center.length < 2) return [];
183
330
  const n = centerlineNormals(center);
@@ -195,7 +342,13 @@ function moduleFootprint(input) {
195
342
  const authored = benchworkOutline(input);
196
343
  const offA = input.endplateTrackOffsets?.["A"] ?? 0;
197
344
  const offB = input.endplateTrackOffsets?.["B"] ?? 0;
198
- const sectionOutlines = sectionFootprints(input);
345
+ const sectionOutlines = sectionFootprints(input, {
346
+ centerline,
347
+ widthA,
348
+ widthB,
349
+ offsetA: offA,
350
+ offsetB: offB
351
+ });
199
352
  return {
200
353
  centerline,
201
354
  band: benchworkBand(centerline, widthA, widthB, offA, offB),
@@ -1116,6 +1269,6 @@ function poseOverridesFromDoc(doc) {
1116
1269
  return out;
1117
1270
  }
1118
1271
 
1119
- export { FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionFootprints, stateToDoc, trackPath };
1272
+ export { FREEMO_ENDPLATE_TRACK_FASCIA_CLEARANCE_INCHES, FREEMO_ENDPLATE_WIDTH_MIN_INCHES, FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, FREEMO_TRACK_SPACING_INCHES, MAIN2_TRACK_ID, MAIN_TRACK_ID, N_CAR_LENGTH_INCHES, N_SCALE_RATIO, asModuleSchematic, benchworkBand, benchworkOutline, buildCrossover, buildPassingSiding, buildTransition, carCapacity, checkEndplateWidth, deriveEndplatePoses, divergeSideForHand, docToState, emptyEditorState, endplateFaceSegments, endplateTrackOffsetFor, endplateTrackOffsetInches, endplateWidthInches, geometryTurnDegrees, inchesToScaleFeet, isLoopDoc, isTransitionTurnout, moduleCenterline, moduleFeatures, moduleFootprint, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionBand, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, sliceCenterline, stateToDoc, trackPath };
1120
1273
  //# sourceMappingURL=index.js.map
1121
1274
  //# sourceMappingURL=index.js.map