ai-chatbot/packages/nodes/src/column/floorplan-affordances.ts
Wassim SAMAD 8abfc94b99
feat(editor): preset-system polish — paint panel, slim action bar, icon rail (#354)
* feat(editor): preset-system polish — paint panel, slim action bar, icon rail

- Export `MaterialPaintPanel` so embedders host the paint material picker in
  their own panel (community docks it in the Build sidebar) instead of the
  bottom action bar.
- ActionMenu: drop the build / material-paint / furnish modes and the
  structure-tools palette row + paint tray (the host's Build sidebar owns
  building now). Reduce `structure-tools` to the shared `tools` lookup still
  used by cursor/floorplan indicators; remove the orphaned `useContextualTools`.
- MaterialPicker: swatches wrap into a fluid `auto-fill` grid that fills width.
- IconRail (tab-bar): bigger icons, grayscale-when-idle, Radix tooltip, w-14
  rail; sync `RAIL_WIDTH` to 56.
- Inspector footer: hand the host `footer` to kind-owned custom panels via
  `InspectorFooterContext` so the save button renders without per-kind wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: biome format pass

Whole-repo formatter normalization (line wrap/unwrap only, no logic changes)
surfaced by the format-on-edit hook against prior drift. Kept separate from the
feature commit so the preset-system diff stays reviewable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:13:23 -04:00

167 lines
5.6 KiB
TypeScript

import {
type AnyNodeId,
type ColumnNode,
type FloorplanAffordance,
useScene,
} from '@pascal-app/core'
// Floor minimums — mirror the 3D handles in `column/definition.ts` so a
// drag can't push a value past what the renderer accepts.
const MIN_COLUMN_WIDTH = 0.1
const MIN_COLUMN_DEPTH = 0.1
const MIN_COLUMN_RADIUS = 0.05
const MIN_BRACE_DIMENSION = 0.04
const MIN_BRACE_BOTTOM_SPREAD = 0.2
const MIN_BRACE_TOP_SPREAD = 0
/**
* One drag pattern for every column size handle. The arrow's outward
* direction in plan coords (`planAxis`) is captured at emit-time; the
* affordance projects the cursor along that axis and applies the delta
* to the dimension named by `dim`.
*
* Sign / factor mirror the 3D `column/definition.ts` handles:
*
* - `width` / `depth` / `uniform` / `brace-width` / `brace-depth` /
* `brace-bottom-spread` / `brace-top-spread`: `anchor: 'center'` —
* a cursor delta of `d` along the outward axis grows the dimension
* by `2·d` (both faces move ±d so the centre stays put).
* - `radius`: `kind: 'radial-resize'` — cursor delta `d` grows the
* radius by `d` (the visible edge follows the cursor 1:1).
*/
export type ColumnResizePayload = {
dim:
| 'width'
| 'depth'
| 'uniform'
| 'radius'
| 'brace-width'
| 'brace-depth'
| 'brace-bottom-spread'
| 'brace-top-spread'
planAxis: [number, number]
}
export const columnResizeAffordance: FloorplanAffordance<ColumnNode> = {
start({ node, payload, initialPlanPoint }) {
const { dim, planAxis } = payload as ColumnResizePayload
const columnId = node.id as AnyNodeId
const [ax, ay] = planAxis
const initialProj = initialPlanPoint[0] * ax + initialPlanPoint[1] * ay
const initialWidth = node.width
const initialDepth = node.depth
const initialRadius = node.radius
const initialBraceWidth = node.braceWidth ?? node.width
const initialBraceDepth = node.braceDepth ?? node.depth
const initialBraceBottomSpread = node.braceBottomSpread ?? Math.max(node.width * 3, 1.2)
const initialBraceTopSpread = node.braceTopSpread ?? 0.12
let lastPatch: Partial<ColumnNode> = {}
const commitPatch = (patch: Partial<ColumnNode>) => {
lastPatch = patch
useScene.getState().updateNode(columnId, patch)
}
return {
affectedIds: [columnId],
apply({ planPoint }) {
const currentProj = planPoint[0] * ax + planPoint[1] * ay
const projDelta = currentProj - initialProj
switch (dim) {
case 'width':
commitPatch({
width: Math.max(MIN_COLUMN_WIDTH, initialWidth + 2 * projDelta),
})
return
case 'depth':
commitPatch({
depth: Math.max(MIN_COLUMN_DEPTH, initialDepth + 2 * projDelta),
})
return
case 'uniform': {
const next = Math.max(MIN_COLUMN_WIDTH, initialWidth + 2 * projDelta)
commitPatch({ width: next, depth: next })
return
}
case 'radius':
commitPatch({
radius: Math.max(MIN_COLUMN_RADIUS, initialRadius + projDelta),
})
return
case 'brace-width':
commitPatch({
braceWidth: Math.max(MIN_BRACE_DIMENSION, initialBraceWidth + 2 * projDelta),
})
return
case 'brace-depth':
commitPatch({
braceDepth: Math.max(MIN_BRACE_DIMENSION, initialBraceDepth + 2 * projDelta),
})
return
case 'brace-bottom-spread':
commitPatch({
braceBottomSpread: Math.max(
MIN_BRACE_BOTTOM_SPREAD,
initialBraceBottomSpread + 2 * projDelta,
),
})
return
case 'brace-top-spread':
commitPatch({
braceTopSpread: Math.max(MIN_BRACE_TOP_SPREAD, initialBraceTopSpread + 2 * projDelta),
})
return
}
},
canCommit() {
return true
},
commit() {
if (Object.keys(lastPatch).length > 0) {
useScene.getState().updateNode(columnId, lastPatch)
}
},
}
},
}
/**
* Column rotation drag (floor-plan). Sister to the 3D
* `columnRotateHandle` (arc-resize). Same `- delta` convention as the
* 3D handle: the floor-plan builder plots the footprint at
* `-column.rotation` (see `buildColumnFloorplan`'s `rot = -node.rotation`),
* so the 2D view rotates the same direction as 3D for the same
* `rotation` value, and the same cursor gesture writes the same sign
* in both views.
*/
export const columnRotateAffordance: FloorplanAffordance<ColumnNode> = {
start({ node, initialPlanPoint }) {
const columnId = node.id as AnyNodeId
const initialRotation = node.rotation ?? 0
const cx = node.position[0]
const cz = node.position[2]
const initialAngle = Math.atan2(initialPlanPoint[1] - cz, initialPlanPoint[0] - cx)
let lastRotation = initialRotation
return {
affectedIds: [columnId],
apply({ planPoint }) {
const currentAngle = Math.atan2(planPoint[1] - cz, planPoint[0] - cx)
let delta = currentAngle - initialAngle
while (delta > Math.PI) delta -= 2 * Math.PI
while (delta < -Math.PI) delta += 2 * Math.PI
const newRotation = initialRotation - delta
lastRotation = newRotation
useScene.getState().updateNode(columnId, { rotation: newRotation })
},
canCommit() {
return true
},
commit() {
useScene.getState().updateNode(columnId, { rotation: lastRotation })
},
}
},
}