@willcgage/module-schematic 0.30.0 → 0.32.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". */
@@ -314,14 +326,18 @@ declare function moduleSections(doc: {
314
326
  * bring in real clipping. */
315
327
  declare function sectionFootprints(doc: {
316
328
  sections?: SchematicSection[] | null;
317
- } | null | undefined): {
318
- id: string;
319
- name?: string;
320
- outline: {
321
- x: number;
322
- y: number;
323
- }[];
324
- }[];
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[];
325
341
  /**
326
342
  * Expand a benchwork outline (whose edges may be arcs) into a dense closed
327
343
  * polyline for rendering — the SAME sampling both the Repository preview and
@@ -387,17 +403,10 @@ interface ModuleFootprint {
387
403
  /** Authored outline (arc-sampled closed ring) or null → render the band.
388
404
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
389
405
  outline: BenchworkPoint[] | null;
390
- /** 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
391
407
  * they are the module's footprint. Empty = this module doesn't use sections,
392
408
  * so fall back to `outline ?? band` exactly as before. */
393
- sectionOutlines: {
394
- id: string;
395
- name?: string;
396
- outline: {
397
- x: number;
398
- y: number;
399
- }[];
400
- }[];
409
+ sectionOutlines: SectionFootprint[];
401
410
  }
402
411
  /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
403
412
  * An authored `mainPath` wins — the owner drew the real shape; otherwise the
@@ -434,6 +443,48 @@ declare function sectionedCenterline(doc: {
434
443
  } | null | undefined): BenchworkPoint[];
435
444
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
436
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[];
458
+ /** Two sections that physically meet, and how much edge they share. */
459
+ interface SectionAdjacency {
460
+ a: string;
461
+ b: string;
462
+ /** Inches of shared edge — a butt joint across a 24″ board reads ~24. */
463
+ lengthInches: number;
464
+ }
465
+ /**
466
+ * Which sections physically MEET, derived from shared polygon edges rather
467
+ * than list order (#96 phase 2c).
468
+ *
469
+ * Order is the wrong model as soon as a module stops being a row of boards: a
470
+ * peninsula hangs off the BACK of a shallow band over part of its length, so
471
+ * it neighbours a board it isn't next to in any list. Geometry is the only
472
+ * thing that knows.
473
+ */
474
+ declare function sectionAdjacency(footprints: SectionFootprint[], opts?: {
475
+ gapInches?: number;
476
+ angleDegrees?: number;
477
+ minOverlapInches?: number;
478
+ }): SectionAdjacency[];
479
+ /** The sections each one touches. */
480
+ declare function sectionNeighbours(id: string, adj: SectionAdjacency[]): string[];
481
+ /**
482
+ * Groups of sections that hang together, as connected components. One group
483
+ * means the module is a single piece of bench work; more than one means some
484
+ * board is floating free — an authoring mistake now, and the test #96 phase 3
485
+ * needs before it can say whether dropping a section leaves the rest intact.
486
+ */
487
+ declare function sectionComponents(ids: string[], adj: SectionAdjacency[]): string[][];
437
488
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
438
489
  declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
439
490
  /**
@@ -920,4 +971,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
920
971
  heading: number;
921
972
  }>;
922
973
 
923
- 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, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, stateToDoc, trackPath };
974
+ 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 SectionAdjacency, 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, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, 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". */
@@ -314,14 +326,18 @@ declare function moduleSections(doc: {
314
326
  * bring in real clipping. */
315
327
  declare function sectionFootprints(doc: {
316
328
  sections?: SchematicSection[] | null;
317
- } | null | undefined): {
318
- id: string;
319
- name?: string;
320
- outline: {
321
- x: number;
322
- y: number;
323
- }[];
324
- }[];
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[];
325
341
  /**
326
342
  * Expand a benchwork outline (whose edges may be arcs) into a dense closed
327
343
  * polyline for rendering — the SAME sampling both the Repository preview and
@@ -387,17 +403,10 @@ interface ModuleFootprint {
387
403
  /** Authored outline (arc-sampled closed ring) or null → render the band.
388
404
  * Null too when `sectionOutlines` is non-empty — the sections ARE the shape. */
389
405
  outline: BenchworkPoint[] | null;
390
- /** 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
391
407
  * they are the module's footprint. Empty = this module doesn't use sections,
392
408
  * so fall back to `outline ?? band` exactly as before. */
393
- sectionOutlines: {
394
- id: string;
395
- name?: string;
396
- outline: {
397
- x: number;
398
- y: number;
399
- }[];
400
- }[];
409
+ sectionOutlines: SectionFootprint[];
401
410
  }
402
411
  /** Module-local main track centre-line (A→B), sampling arcs for curves/corners.
403
412
  * An authored `mainPath` wins — the owner drew the real shape; otherwise the
@@ -434,6 +443,48 @@ declare function sectionedCenterline(doc: {
434
443
  } | null | undefined): BenchworkPoint[];
435
444
  /** Benchwork band: the centre-line offset ±half-width, tapering widthA→widthB. */
436
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[];
458
+ /** Two sections that physically meet, and how much edge they share. */
459
+ interface SectionAdjacency {
460
+ a: string;
461
+ b: string;
462
+ /** Inches of shared edge — a butt joint across a 24″ board reads ~24. */
463
+ lengthInches: number;
464
+ }
465
+ /**
466
+ * Which sections physically MEET, derived from shared polygon edges rather
467
+ * than list order (#96 phase 2c).
468
+ *
469
+ * Order is the wrong model as soon as a module stops being a row of boards: a
470
+ * peninsula hangs off the BACK of a shallow band over part of its length, so
471
+ * it neighbours a board it isn't next to in any list. Geometry is the only
472
+ * thing that knows.
473
+ */
474
+ declare function sectionAdjacency(footprints: SectionFootprint[], opts?: {
475
+ gapInches?: number;
476
+ angleDegrees?: number;
477
+ minOverlapInches?: number;
478
+ }): SectionAdjacency[];
479
+ /** The sections each one touches. */
480
+ declare function sectionNeighbours(id: string, adj: SectionAdjacency[]): string[];
481
+ /**
482
+ * Groups of sections that hang together, as connected components. One group
483
+ * means the module is a single piece of bench work; more than one means some
484
+ * board is floating free — an authoring mistake now, and the test #96 phase 3
485
+ * needs before it can say whether dropping a section leaves the rest intact.
486
+ */
487
+ declare function sectionComponents(ids: string[], adj: SectionAdjacency[]): string[][];
437
488
  /** The two endplate faces (the band's flat ends): [A end at widthA, B end at widthB]. */
438
489
  declare function endplateFaceSegments(center: BenchworkPoint[], widthA?: number, widthB?: number, offsetA?: number, offsetB?: number): OutlineFace[];
439
490
  /**
@@ -920,4 +971,4 @@ declare function poseOverridesFromDoc(doc: ModuleSchematicDoc): Record<string, {
920
971
  heading: number;
921
972
  }>;
922
973
 
923
- 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, moduleLengthFromSections, moduleSections, nextId, poseNeedsManual, poseOverridesFromDoc, sampleBenchworkOutline, samplePath, scaleFeetToInches, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, stateToDoc, trackPath };
974
+ 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 SectionAdjacency, 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, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionedCenterline, sliceCenterline, stateToDoc, trackPath };
package/dist/index.js CHANGED
@@ -33,12 +33,26 @@ function moduleSections(doc) {
33
33
  };
34
34
  });
35
35
  }
36
- function sectionFootprints(doc) {
37
- return moduleSections(doc).filter((sec) => sec.outline).map((sec) => ({
38
- id: sec.id,
39
- ...sec.name ? { name: sec.name } : {},
40
- outline: sampleBenchworkOutline(sec.outline)
41
- }));
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);
42
56
  }
43
57
  function sampleBenchworkOutline(pts, segsPerArc = 20) {
44
58
  const n = pts.length;
@@ -254,6 +268,136 @@ function benchworkBand(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES
254
268
  }));
255
269
  return [...left, ...right.reverse()];
256
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
+ }
328
+ function ringEdges(pts) {
329
+ const out = [];
330
+ for (let i = 0; i < pts.length; i++) out.push([pts[i], pts[(i + 1) % pts.length]]);
331
+ return out;
332
+ }
333
+ function sharedEdgeLength(e1, e2, gap, angleDeg) {
334
+ const ux = e1[1].x - e1[0].x;
335
+ const uy = e1[1].y - e1[0].y;
336
+ const ul = Math.hypot(ux, uy);
337
+ const vx = e2[1].x - e2[0].x;
338
+ const vy = e2[1].y - e2[0].y;
339
+ const vl = Math.hypot(vx, vy);
340
+ if (ul < 1e-6 || vl < 1e-6) return 0;
341
+ const dx = ux / ul;
342
+ const dy = uy / ul;
343
+ const cross = Math.abs((dx * vy - dy * vx) / vl);
344
+ if (cross > Math.sin(angleDeg * Math.PI / 180)) return 0;
345
+ const perp = (q) => Math.abs((q.x - e1[0].x) * -dy + (q.y - e1[0].y) * dx);
346
+ if (perp(e2[0]) > gap || perp(e2[1]) > gap) return 0;
347
+ const proj = (q) => (q.x - e1[0].x) * dx + (q.y - e1[0].y) * dy;
348
+ const t1 = proj(e2[0]);
349
+ const t2 = proj(e2[1]);
350
+ return Math.max(0, Math.min(ul, Math.max(t1, t2)) - Math.max(0, Math.min(t1, t2)));
351
+ }
352
+ function sectionAdjacency(footprints, opts) {
353
+ const gap = opts?.gapInches ?? 0.5;
354
+ const angle = opts?.angleDegrees ?? 3;
355
+ const min = opts?.minOverlapInches ?? 1;
356
+ const edges = footprints.map((f) => ringEdges(f.outline));
357
+ const out = [];
358
+ for (let i = 0; i < footprints.length; i++) {
359
+ for (let j = i + 1; j < footprints.length; j++) {
360
+ let total = 0;
361
+ for (const e1 of edges[i])
362
+ for (const e2 of edges[j]) total += sharedEdgeLength(e1, e2, gap, angle);
363
+ if (total >= min)
364
+ out.push({
365
+ a: footprints[i].id,
366
+ b: footprints[j].id,
367
+ lengthInches: Math.round(total * 1e3) / 1e3
368
+ });
369
+ }
370
+ }
371
+ return out;
372
+ }
373
+ function sectionNeighbours(id, adj) {
374
+ return adj.filter((x) => x.a === id || x.b === id).map((x) => x.a === id ? x.b : x.a);
375
+ }
376
+ function sectionComponents(ids, adj) {
377
+ const parent = new Map(ids.map((id) => [id, id]));
378
+ const find = (x) => {
379
+ let r = x;
380
+ while (parent.get(r) !== r) r = parent.get(r);
381
+ while (parent.get(x) !== r) {
382
+ const nx = parent.get(x);
383
+ parent.set(x, r);
384
+ x = nx;
385
+ }
386
+ return r;
387
+ };
388
+ for (const { a, b } of adj) {
389
+ if (!parent.has(a) || !parent.has(b)) continue;
390
+ const ra = find(a);
391
+ const rb = find(b);
392
+ if (ra !== rb) parent.set(ra, rb);
393
+ }
394
+ const groups = /* @__PURE__ */ new Map();
395
+ for (const id of ids) {
396
+ const r = find(id);
397
+ groups.set(r, [...groups.get(r) ?? [], id]);
398
+ }
399
+ return [...groups.values()];
400
+ }
257
401
  function endplateFaceSegments(center, widthA = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, widthB = FREEMO_ENDPLATE_WIDTH_RECOMMENDED_INCHES, offsetA = 0, offsetB = 0) {
258
402
  if (center.length < 2) return [];
259
403
  const n = centerlineNormals(center);
@@ -271,7 +415,13 @@ function moduleFootprint(input) {
271
415
  const authored = benchworkOutline(input);
272
416
  const offA = input.endplateTrackOffsets?.["A"] ?? 0;
273
417
  const offB = input.endplateTrackOffsets?.["B"] ?? 0;
274
- const sectionOutlines = sectionFootprints(input);
418
+ const sectionOutlines = sectionFootprints(input, {
419
+ centerline,
420
+ widthA,
421
+ widthB,
422
+ offsetA: offA,
423
+ offsetB: offB
424
+ });
275
425
  return {
276
426
  centerline,
277
427
  band: benchworkBand(centerline, widthA, widthB, offA, offB),
@@ -1192,6 +1342,6 @@ function poseOverridesFromDoc(doc) {
1192
1342
  return out;
1193
1343
  }
1194
1344
 
1195
- 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, sectionBreaksFromSections, sectionFootprints, sectionSpans, sectionedCenterline, stateToDoc, trackPath };
1345
+ 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, sectionAdjacency, sectionBand, sectionBreaksFromSections, sectionComponents, sectionFootprints, sectionNeighbours, sectionSpans, sectionedCenterline, sliceCenterline, stateToDoc, trackPath };
1196
1346
  //# sourceMappingURL=index.js.map
1197
1347
  //# sourceMappingURL=index.js.map