feat(fence): add horizontal-board fence style with capped posts

Adds a `horizontal` fence style that builds composite-cladding panels —
horizontal boards stacked between square intermediate posts — instead of
the vertical pickets that slat/rail/privacy all produce today.

- New `style: 'horizontal'` (core schema + inspector Style control).
- Posts march along the whole span at `postSpacing`, each with a cap.
- New `postCap` control (none/flat/pyramid, default pyramid); pyramid
  caps render as a 4-sided cone fence part.
- New `slatGap` control for the board reveal — 0 collapses to one flush
  panel so the stacked-board seams don't read as lines.
- `postCap` + `slatGap` are horizontal-only (visibleIf); postCap uses a
  labelled dropdown so its options aren't a context-free switch.
- Lower the `postSpacing` inspector minimum from 0.2 to 0.05 m.
- 2D floor plan reuses the solid-panel marker for horizontal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-19 12:57:24 -04:00
co-authored by Claude Opus 4.8
parent ee4d6de206
commit 346b5d8cbe
5 changed files with 157 additions and 7 deletions
+6 -1
View File
@@ -3,8 +3,9 @@ import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'
export const FenceStyle = z.enum(['slat', 'rail', 'privacy'])
export const FenceStyle = z.enum(['slat', 'rail', 'privacy', 'horizontal'])
export const FenceBaseStyle = z.enum(['floating', 'grounded'])
export const FencePostCap = z.enum(['none', 'flat', 'pyramid'])
export const FenceNode = BaseNode.extend({
id: objectId('fence'),
@@ -25,6 +26,10 @@ export const FenceNode = BaseNode.extend({
topRailHeight: z.number().default(0.04),
groundClearance: z.number().default(0),
edgeInset: z.number().default(0.015),
// Reveal between the boards of a `horizontal` fence (0 = flush cladding).
slatGap: z.number().default(0.01),
// Topper drawn on each `horizontal`-fence post.
postCap: FencePostCap.default('pyramid'),
baseStyle: FenceBaseStyle.default('grounded'),
showInfill: z.boolean().default(true),
color: z.string().default('#ffffff'),