Skip to content

feat(bin-designer): add corrugated wall pattern#1291

Closed
andymai wants to merge 2 commits intomainfrom
feat/corrugated-wall-pattern
Closed

feat(bin-designer): add corrugated wall pattern#1291
andymai wants to merge 2 commits intomainfrom
feat/corrugated-wall-pattern

Conversation

@andymai
Copy link
Copy Markdown
Owner

@andymai andymai commented Mar 29, 2026

Summary

  • Adds a corrugated wall pattern — sinusoidal wave profiles that replace flat walls for added structural strength
  • New wallReplaceTargets pipeline concept: cut flat wall section, then fuse corrugated replacement (unlike honeycomb which is purely subtractive)
  • Pattern registry widened to a discriminated union (cut vs replace modes) for clean extensibility
  • Phase-aligned wavelength ensures wave crests at wall corners for clean geometry
  • Size-adaptive parameters: amplitude = wallThickness × 0.4, wavelength scales with bin height (8/12/16mm tiers)
  • Minimum 1.6mm wall thickness enforced via per-option disable in the pattern selector UI
  • Clear zones around cutouts/handles/ramps match honeycomb's CUTOUT_BORDER_WIDTH clipping behavior
  • i18n: all 7 locales updated (de, en, es, fr, nb, nl, pt-BR)

Files

Created (4):

  • patterns/corrugatedPattern.ts — pure math: spec computation, wave profile points
  • patterns/corrugatedPattern.test.ts — 14 unit tests
  • corrugatedWallBuilder.ts — geometry: profile → solid, slab, clip zones, per-wall caching
  • corrugatedWallBuilder.test.ts — 5 spec integration tests

Modified (13): types, registry, pipeline context/stages, constraints, UI selector, i18n

Test plan

  • pnpm run typecheck — passes
  • pnpm run lint — 0 errors
  • pnpm run check:i18n — all 4 checks pass
  • All 721 generation tests pass (47 files)
  • All 109 affected tests pass (patterns, constraints, WallsSection)
  • Manual: verify corrugated wall renders correctly in 3D preview
  • Manual: verify corrugated + cutout/handle clear zones
  • Manual: verify thickness slider auto-disables corrugated below 1.6mm

Add a sinusoidal corrugated wall pattern as a new option alongside
honeycomb. Unlike honeycomb (which cuts holes), corrugated replaces
flat walls with uniform-thickness wave profiles folding inward from
the grid boundary for added structural strength.

- New discriminated union registry (cut vs replace pattern modes)
- New wallReplaceTargets pipeline concept (cut flat + fuse corrugated)
- Phase-aligned wavelength ensures wave crests at wall corners
- Size-adaptive parameters: amplitude = thickness × 0.4, wavelength
  scales with bin height
- Minimum 1.6mm wall thickness enforced in UI (per-option disable)
- Clear zones around cutouts/handles/ramps match honeycomb behavior
- i18n: all 7 locales updated
Copilot AI review requested due to automatic review settings March 29, 2026 00:22
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gridfinity-layout-tool Ready Ready Preview, Comment Mar 29, 2026 0:46am

Request Review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new corrugated wall pattern to the bin designer and generation pipeline, introducing a new “wall replacement” boolean pass (cut flat wall slab → fuse corrugated wall solid) alongside the existing cut-through pattern approach.

Changes:

  • Introduces corrugated pattern math + geometry builders and routes pattern application through a new wallReplaceTargets pipeline concept.
  • Extends the pattern registry to a discriminated union (cut vs replace) and updates the pipeline stages accordingly.
  • Updates the Walls UI to include corrugated selection, per-option disabling under 1.6mm wall thickness, and updates i18n strings across locales.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/i18n/locales/pt-BR.json Adds corrugated pattern labels and thin-wall warning.
src/i18n/locales/nl.json Adds corrugated pattern labels and thin-wall warning.
src/i18n/locales/nb.json Adds corrugated pattern labels and thin-wall warning.
src/i18n/locales/fr.json Adds corrugated pattern labels and thin-wall warning.
src/i18n/locales/es.json Adds corrugated pattern labels and thin-wall warning.
src/i18n/locales/en.ts Adds corrugated pattern labels and thin-wall warning (TS locale).
src/i18n/locales/en.json Adds corrugated pattern labels and thin-wall warning (JSON locale).
src/i18n/locales/de.json Adds corrugated pattern labels and thin-wall warning.
src/features/generation/worker/generators/pipeline/types.ts Extends pipeline context with wallReplaceTargets.
src/features/generation/worker/generators/pipeline/stages/featuresStage.ts Branches wall pattern building by registry mode (cut vs replace).
src/features/generation/worker/generators/pipeline/stages/booleanStage.ts Applies the new wall replacement boolean pass and cleans up shapes.
src/features/generation/worker/generators/pipeline/context.ts Initializes wallReplaceTargets in the initial context.
src/features/generation/worker/generators/patterns/registry.ts Converts registry entries to a discriminated union and adds corrugated entry.
src/features/generation/worker/generators/patterns/index.ts Re-exports corrugated helpers and adds getPatternMode.
src/features/generation/worker/generators/patterns/corrugatedPattern.ts Implements corrugated spec computation and profile point generation.
src/features/generation/worker/generators/patterns/corrugatedPattern.test.ts Adds unit tests for corrugated spec/point generation.
src/features/generation/worker/generators/corrugatedWallBuilder.ts Builds corrugated wall solids + cut slabs with clear-zone clipping and caching.
src/features/generation/worker/generators/corrugatedWallBuilder.test.ts Adds basic integration/spec tests for corrugated wall spec behavior.
src/features/bin-designer/types/index.ts Extends WallPatternType to include corrugated.
src/features/bin-designer/components/panel/WallsSection/useWallsSection.ts Adds per-pattern disable logic and auto-disables corrugated when too thin.
src/features/bin-designer/components/panel/WallsSection/WallsSection.tsx Passes disabledPatterns into the pattern selector.
src/features/bin-designer/components/panel/WallsSection/PatternSelector.tsx Adds corrugated option + icon and supports individually disabled options.
src/features/bin-designer/components/panel/WallsSection/PatternSelector.test.tsx Updates tests for the new option and disabled-pattern behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +118 to +135
try {
const prev = bin;
bin = unwrap(cut(bin as ValidSolid, cutShape as ValidSolid));
if (prev !== originalSolid) prev.delete();
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If cut fails, skip this wall replacement
fuseShape.delete();
cutShape.delete();
continue;
}
try {
const prev = bin;
bin = unwrap(fuse(bin as ValidSolid, fuseShape as ValidSolid));
if (prev !== originalSolid) prev.delete();
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If fuse fails, keep the cut result (flat wall is removed, no corrugated added)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the wall replacement loop, if the cut succeeds but the subsequent fuse fails, the code intentionally keeps the cut result, which can leave a missing wall section (a hole) in the final solid. To avoid producing invalid/unsafe geometry, keep the pre-cut solid unless both operations succeed (e.g., only delete/discard prev after a successful fuse, or roll back to prev on fuse failure).

Suggested change
try {
const prev = bin;
bin = unwrap(cut(bin as ValidSolid, cutShape as ValidSolid));
if (prev !== originalSolid) prev.delete();
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If cut fails, skip this wall replacement
fuseShape.delete();
cutShape.delete();
continue;
}
try {
const prev = bin;
bin = unwrap(fuse(bin as ValidSolid, fuseShape as ValidSolid));
if (prev !== originalSolid) prev.delete();
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If fuse fails, keep the cut result (flat wall is removed, no corrugated added)
let cutResult: Shape3D | null = null;
try {
// Perform cut into a temporary solid; do not update bin yet.
cutResult = unwrap(cut(bin as ValidSolid, cutShape as ValidSolid));
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If cut fails, skip this wall replacement entirely.
// Eagerly dispose these shapes; final cleanup is defensive.
try {
fuseShape.delete();
} catch {
/* already disposed */
}
try {
cutShape.delete();
} catch {
/* already disposed */
}
continue;
}
try {
// Fuse the replacement wall into the cut result.
const fused = unwrap(fuse(cutResult as ValidSolid, fuseShape as ValidSolid));
// Both cut and fuse succeeded: now atomically update bin.
if (bin !== originalSolid) {
try {
bin.delete();
} catch {
/* already disposed */
}
}
try {
cutResult.delete();
} catch {
/* already disposed */
}
bin = fused;
cutResult = null;
} catch (e: unknown) {
if (isAbortError(e)) throw e;
// If fuse fails, discard the cut result and keep the original bin
if (cutResult) {
try {
cutResult.delete();
} catch {
/* already disposed */
}
cutResult = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +92
* Outer face is at Y=0 (flat line), inner face follows the sine wave.
*/
function buildCorrugatedProfile(spec: CorrugatedWallSpec): Drawing {
const innerPoints = generateInnerFacePoints(spec);
const halfSpan = spec.wallSpan / 2;

// In the bin's coordinate system, walls extend outward (negative Y from
// the inner face for the front wall). The profile Y axis is negated so
// the geometry occupies the wall region, not the bin cavity.
//
// Y=0 = inner face (wall anchor point)
// Y=-wallThickness = outer face (flat, at grid boundary)
// Y=-wallThickness-amplitude = deepest corrugation point

// Start at bottom-left of inner face
let pen = draw([-halfSpan, 0]);

// Inner face: left to right (flat line at Y=0, the inner face boundary)
pen = pen.lineTo([halfSpan, 0]);

// Right edge: step outward to outer sinusoidal face
const lastInner = innerPoints[innerPoints.length - 1];
pen = pen.lineTo([lastInner[0], -lastInner[1]]);

// Outer face: right to left (sinusoidal wave, negated Y)
for (let i = innerPoints.length - 2; i >= 0; i--) {
pen = pen.lineTo([innerPoints[i][0], -innerPoints[i][1]]);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildCorrugatedProfile/its comments describe the outer face as flat at the grid boundary, but the polygon being constructed makes the Y=0 edge flat and the other edge follow generateInnerFacePoints (negated). Since generateInnerFacePoints is defined as the inner face depth from a flat outer face, this effectively makes the outer wall boundary wavy and can change the bin’s external dimensions. Please realign the profile so the outer face remains fixed (matching the existing shell) and the corrugation folds inward.

Suggested change
* Outer face is at Y=0 (flat line), inner face follows the sine wave.
*/
function buildCorrugatedProfile(spec: CorrugatedWallSpec): Drawing {
const innerPoints = generateInnerFacePoints(spec);
const halfSpan = spec.wallSpan / 2;
// In the bin's coordinate system, walls extend outward (negative Y from
// the inner face for the front wall). The profile Y axis is negated so
// the geometry occupies the wall region, not the bin cavity.
//
// Y=0 = inner face (wall anchor point)
// Y=-wallThickness = outer face (flat, at grid boundary)
// Y=-wallThickness-amplitude = deepest corrugation point
// Start at bottom-left of inner face
let pen = draw([-halfSpan, 0]);
// Inner face: left to right (flat line at Y=0, the inner face boundary)
pen = pen.lineTo([halfSpan, 0]);
// Right edge: step outward to outer sinusoidal face
const lastInner = innerPoints[innerPoints.length - 1];
pen = pen.lineTo([lastInner[0], -lastInner[1]]);
// Outer face: right to left (sinusoidal wave, negated Y)
for (let i = innerPoints.length - 2; i >= 0; i--) {
pen = pen.lineTo([innerPoints[i][0], -innerPoints[i][1]]);
* Outer face is at Y=0 (flat line at the existing shell boundary),
* inner face follows the sine wave inward (positive depth).
*/
function buildCorrugatedProfile(spec: CorrugatedWallSpec): Drawing {
const innerPoints = generateInnerFacePoints(spec);
const halfSpan = spec.wallSpan / 2;
// Y=0 = outer face (flat, at grid boundary)
// Y>0 = into wall thickness (towards bin interior), following corrugation
// Start at left of outer face
let pen = draw([-halfSpan, 0]);
// Outer face: left to right (flat line at Y=0)
pen = pen.lineTo([halfSpan, 0]);
// Right edge: step inward to inner sinusoidal face
const lastInner = innerPoints[innerPoints.length - 1];
pen = pen.lineTo([lastInner[0], lastInner[1]]);
// Inner face: right to left (sinusoidal wave, depth from outer face)
for (let i = innerPoints.length - 2; i >= 0; i--) {
pen = pen.lineTo([innerPoints[i][0], innerPoints[i][1]]);

Copilot uses AI. Check for mistakes.
Comment on lines +452 to +488
// Cache key
const wallKey = compactKey(
buildCacheKey(
'v1',
'corrugated',
quantize(spec.amplitude),
quantize(spec.wavelength),
quantize(spec.patternH),
quantize(spec.bottomZ),
quantize(spec.wallSpan),
quantize(spec.wallThickness),
spec.waveCount,
quantize(wallPos.translateX),
quantize(wallPos.translateY),
wallPos.zRotation ?? 0,
// Include cutout/handle config in cache key for clear zones
params.walls.enabled && params.walls[wallPos.side]?.enabled
? buildCacheKey(
'clip',
params.walls.shape,
quantize(params.walls[wallPos.side].width),
quantize(params.walls[wallPos.side].depth),
params.walls[wallPos.side].alignment,
quantize(params.walls[wallPos.side].offset)
)
: 'noclip',
params.handles.enabled && params.handles[wallPos.side]?.enabled
? buildCacheKey(
'hdl',
params.handles.shape,
params.handles.count,
quantize(params.handles[wallPos.side].width ?? params.handles.width),
quantize(params.handles[wallPos.side].height ?? params.handles.height),
quantize(params.handles.verticalPosition ?? 0)
)
: 'nohdl'
)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache key for corrugated walls doesn’t include several inputs that affect the final clipped geometry (e.g., ramp zones from computeRampZones, hasLip/lip overhang, compartment thickness used in clipExtrudeDepth, widthMm vs % width for wall cutouts, and the label.enabled condition that changes handle clipping on the back wall). This can cause stale cached shapes to be reused with incorrect clear zones or insufficient clip depth. Consider mirroring wallPatternBuilder’s approach: compute the effective clip params (including ramp zones + handle segments) and incorporate those derived values into the cache key (and bump the cache version).

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +99
// Phase-align: snap to integer number of complete waves
const waveCount = Math.max(1, Math.round(wallSpan / baseWavelength));
const wavelength = wallSpan / waveCount;

// Must fit at least one half-wave vertically
if (patternH < wavelength / 2) return null;

// Wall span must be meaningful (need at least one full wave)
if (wallSpan < baseWavelength / 2) return null;

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createCorrugatedSpec rejects cases where patternH < wavelength / 2, but wavelength is computed from the wall span (horizontal corrugation), while patternH is the vertical extrusion height used by the builder. Since the corrugation is extruded uniformly in Z (no vertical wave), this mixes unrelated dimensions and can disable corrugation for short bins based solely on wall span. Either remove/replace this constraint with a height-based minimum that matches the actual geometry, or update the implementation if the intent is a vertical waveform.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +10
/** Minimum wall thickness for corrugated pattern (mm). Must match corrugatedPattern.ts. */
const CORRUGATED_MIN_WALL_THICKNESS = 1.6;
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corrugated min wall thickness is duplicated here as a local constant with a “must match” comment, while the generation layer already exports CORRUGATED_MIN_WALL_THICKNESS. To prevent drift between UI gating and generator behavior, consider defining this threshold in a shared/constants module (importable by both UI and worker) and referencing it from both places instead of duplicating the numeric literal.

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 29, 2026

Greptile Summary

This PR adds a corrugated (sinusoidal) wall pattern to the bin designer, complementing the existing honeycomb option. The feature introduces a new replace pipeline concept — rather than purely subtracting holes, it cuts out a flat wall section and fuses a corrugated solid in its place — and correctly wires this through the existing featuresStagebooleanStage pipeline.

Key implementation highlights:

  • Math is correct: phase-aligned wavelength (integer wave count across span), amplitude = wallThickness × 0.4, patternH keep-outs match the existing honeycomb approach using the same MIN_BOTTOM_KEEP_OUT / TOP_KEEP_OUT constants
  • Discriminated union registry (cut | replace) is clean and extensible; getPatternCalculator correctly guards against non-cut entries
  • Atomicity in booleanStage: because brepjs returns new shapes rather than mutating operands, a fuse failure naturally leaves bin unchanged — the per-pair try/catch is a sound defensive pattern
  • Clear-zone clipping faithfully replicates the honeycomb CUTOUT_BORDER_WIDTH approach for cutouts, handles, and ramp zones
  • UI enforcement: the 1.6mm minimum thickness is enforced via per-option disabled on the <option> element and via auto-disabling the active pattern when the slider drops below the threshold
  • i18n: all 7 locales updated; constant is correctly placed in shared/constants/bin.ts so both UI and geometry layers share the same threshold

Two minor style issues found:

  • A test comment in corrugatedWallBuilder.test.ts misattributes the null return of a short-wall spec to the wavelength check rather than the patternH < 2 guard
  • The hint paragraph in PatternSelector.tsx always renders the corrugated-specific i18n key even though disabledPatterns is typed as a generic ReadonlySet<WallPatternType> — guarding on disabledPatterns?.has('corrugated') directly would make the coupling explicit

Confidence Score: 5/5

Safe to merge — all findings are P2 style suggestions with no impact on runtime correctness or geometry output.

The implementation is thorough and well-tested (14 unit tests + 5 integration tests). The math, memory management, pipeline integration, and UI constraint enforcement are all correct. Only two minor documentation/style issues were found, neither of which affects current behavior.

No files require special attention; the two minor suggestions are in PatternSelector.tsx (hint message guard) and corrugatedWallBuilder.test.ts (test comment accuracy).

Important Files Changed

Filename Overview
src/features/generation/worker/generators/patterns/corrugatedPattern.ts New pure-math module: correctly computes wave spec (amplitude, phase-aligned wavelength, keep-out zones) and generates sinusoidal inner-face points; math and edge cases validated by tests.
src/features/generation/worker/generators/corrugatedWallBuilder.ts New geometry builder: profile construction, flat-slab cutting, corrugated fusing, and clear-zone clipping all look correct; memory management mirrors wallPatternBuilder with explicit deletes and try/catch for consumed shapes.
src/features/generation/worker/generators/pipeline/stages/booleanStage.ts New wallReplaceTargets pass handles atomicity correctly — because brepjs returns new shapes rather than mutating operands, a fuse failure leaves bin untouched; cleanup loop safely handles already-consumed shapes via try/catch.
src/features/generation/worker/generators/patterns/registry.ts Clean discriminated union (cut vs replace) widening; getPatternMode fallback to 'cut' is unreachable given TypeScript's Record constraint but safe; getPatternCalculator correctly guards against non-cut entries.
src/features/bin-designer/components/panel/WallsSection/useWallsSection.ts Auto-disable and disabledPatterns memo are correct; wallPattern is properly in the useCallback dependency array; new Set is created on each wallThickness change (acceptable memoization cost).
src/features/bin-designer/components/panel/WallsSection/PatternSelector.tsx Corrugated option added correctly; per-option disabled attribute works; hint message hardcoded to corrugated key regardless of which patterns are in the generic disabledPatterns set — see comment.
src/features/generation/worker/generators/corrugatedWallBuilder.test.ts 5 integration-style spec tests are correct but one test comment incorrectly attributes a null return to the wavelength check instead of the patternH < 2 guard — see comment.
src/features/generation/worker/generators/patterns/corrugatedPattern.test.ts 14 unit tests comprehensively cover spec rejection, amplitude/wavelength computation, phase alignment, and inner-face point bounds; assertions are accurate.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/features/generation/worker/generators/corrugatedWallBuilder.test.ts
Line: 235-239

Comment:
**Misleading test comment attributes null to wrong check**

The comment says `Wavelength for height=4 is ~12mm, half = 6mm > 1.9mm → null`, implying the null return is because `wallSpan < baseWavelength / 2`. But `wallSpan = 80.8mm` easily passes that check — the actual rejection is `patternH = 1.9 < 2`. The second comment line is factually wrong and could mislead a future developer debugging why a spec returned null.

```suggestion
    it('rejects very short walls', () => {
      // Wall height of 5mm with 1.6mm bottom keepout and 1.5mm top = 1.9mm patternH
      // patternH (1.9mm) < minimum required (2mm) → null
      const spec = createCorrugatedSpec(1.6, 5, 80.8, 4);
      expect(spec).toBeNull();
    });
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/features/bin-designer/components/panel/WallsSection/PatternSelector.tsx
Line: 118-122

Comment:
**Hint message hardcoded to corrugated regardless of which patterns are disabled**

The `disabledPatterns` prop is typed as `ReadonlySet<WallPatternType>` (generic), but the rendered message is always `corrugatedThinWall`. If a future pattern is added to `disabledPatterns` for a different constraint, this will display the wrong explanation to users. Right now it works because only `corrugated` is ever added to the set, but the component contract doesn't express that coupling.

One approach is to guard more specifically:

```suggestion
      {!disabled && disabledPatterns?.has('corrugated') && (
        <p className="text-[11px] text-content-tertiary mt-1.5">
          {t('binDesigner.walls.pattern.corrugatedThinWall')}
        </p>
      )}
```

This makes the trigger condition explicit and won't accidentally show the corrugated message if a different pattern is ever added to `disabledPatterns`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix(corrugated): address PR review feedb..." | Re-trigger Greptile

- Atomic cut+fuse: rollback to original bin if fuse fails (no holes)
- Fix profile coordinates: outer face at Y=0, inner face positive Y
- Complete cache key: add ramp zones, lip, compartment thickness, label
- Replace misleading patternH < wavelength/2 with 2mm min height
- Move CORRUGATED_MIN_WALL_THICKNESS to shared/constants/bin.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants