parametrics: add custom field kind + restore fence Length / Curve

User noticed the auto-derived fence inspector was missing the legacy
panel's Length + Curve sliders, and the section labelling was wrong
(Posts → Structure). Both Length and Curve are awkward for the
parametrics field model:

- **Length** doesn't map to a single node key — it's derived from
  `start`/`end`, and editing it moves `end` along the existing
  direction.
- **Curve** maps to `curveOffset` but the slider's min/max are
  bounded per-node by the chord length, plus updates need
  `normalizeWallCurveOffset`.

Adds a `kind: 'custom'` field with a kind-supplied
`component: ComponentType<{ node, onUpdate }>`. The inspector mounts
it and lets the kind own rendering + update logic. `key` becomes a
free-form React key/label since it no longer needs to map to a node
property.

Fence parametrics now mirrors the legacy layout 1:1:
- Style (segmented controls + showInfill toggle).
- Dimensions (Length, Curve, Height, Thickness).
- Structure (Base Height, Top Rail, Post Spacing, Post Size, Ground
  Clear, Edge Inset).

Length + Curve live in fence/inspector-editors.tsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-18 10:27:40 -04:00
co-authored by Claude Opus 4.7
parent 80199dc890
commit 282cb22585
4 changed files with 150 additions and 23 deletions
@@ -293,12 +293,35 @@ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
)
}
case 'custom':
// The field owns its rendering and update logic — used for
// derived values (length from start/end), dynamic-bounded
// sliders (curve sagitta), composed editors.
return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
default:
// material / ref / unrecognized kinds — not implemented in v1.
return null
}
}
function CustomFieldRenderer({
Comp,
nodeId,
onUpdate,
}: {
Comp: ComponentType<{ node: AnyNode; onUpdate: (patch: Partial<AnyNode>) => void }>
nodeId: AnyNodeId
onUpdate: (patch: Partial<AnyNode>) => void
}) {
// Subscribe to the full node — the custom editor may read any
// field. Tools that don't want this churn should write narrower
// selectors inside Comp itself.
const node = useScene((s) => s.nodes[nodeId])
if (!node) return null
return <Comp node={node} onUpdate={onUpdate} />
}
// ─── helpers ─────────────────────────────────────────────────────────
function precisionForStep(step: number): number {