window panel

This commit is contained in:
wass08
2026-02-18 13:14:55 +09:00
parent 076faae7df
commit be013d26e5
5 changed files with 198 additions and 69 deletions
@@ -280,13 +280,15 @@ export const WindowTool: React.FC = () => {
frameDepth: draft.frameDepth,
columnRatios: draft.columnRatios,
rowRatios: draft.rowRatios,
dividerThickness: draft.dividerThickness,
columnDividerThickness: draft.columnDividerThickness,
rowDividerThickness: draft.rowDividerThickness,
sill: draft.sill,
sillDepth: draft.sillDepth,
sillThickness: draft.sillThickness,
})
useScene.getState().createNode(node, event.node.id as AnyNodeId)
useViewer.getState().setSelection({ selectedIds: [node.id] })
useScene.temporal.getState().pause()
event.stopPropagation()
+144 -20
View File
@@ -2,7 +2,7 @@
import { type AnyNode, type AnyNodeId, type WindowNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { X } from 'lucide-react'
import { FlipHorizontal2, X } from 'lucide-react'
import Image from 'next/image'
import { useCallback } from 'react'
import { NumberInput } from '@/components/ui/primitives/number-input'
@@ -23,7 +23,6 @@ export function WindowPanel() {
(updates: Partial<WindowNode>) => {
if (!selectedId) return
updateNode(selectedId as AnyNode['id'], updates)
// Mark dirty so window-system regenerates geometry
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
},
[selectedId, updateNode],
@@ -33,13 +32,53 @@ export function WindowPanel() {
setSelection({ selectedIds: [] })
}, [setSelection])
const handleFlip = useCallback(() => {
if (!node) return
handleUpdate({
side: node.side === 'front' ? 'back' : 'front',
rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
})
}, [node, handleUpdate])
if (!node || node.type !== 'window' || selectedIds.length !== 1) return null
const columns = node.columnRatios.length
const rows = node.rowRatios.length
const numCols = node.columnRatios.length
const numRows = node.rowRatios.length
// Normalized ratios (always sum to 1 for display)
const colSum = node.columnRatios.reduce((a, b) => a + b, 0)
const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
const normCols = node.columnRatios.map(r => r / colSum)
const normRows = node.rowRatios.map(r => r / rowSum)
const setColumnRatio = (index: number, newVal: number) => {
const clamped = Math.max(0.05, Math.min(0.95, newVal))
const neighborIdx = index < numCols - 1 ? index + 1 : index - 1
const delta = clamped - normCols[index]!
const neighborVal = Math.max(0.05, normCols[neighborIdx]! - delta)
const newRatios = normCols.map((v, i) => {
if (i === index) return clamped
if (i === neighborIdx) return neighborVal
return v
})
handleUpdate({ columnRatios: newRatios })
}
const setRowRatio = (index: number, newVal: number) => {
const clamped = Math.max(0.05, Math.min(0.95, newVal))
const neighborIdx = index < numRows - 1 ? index + 1 : index - 1
const delta = clamped - normRows[index]!
const neighborVal = Math.max(0.05, normRows[neighborIdx]! - delta)
const newRatios = normRows.map((v, i) => {
if (i === index) return clamped
if (i === neighborIdx) return neighborVal
return v
})
handleUpdate({ rowRatios: newRatios })
}
return (
<div className="pointer-events-auto fixed top-20 right-4 z-50 flex w-72 flex-col overflow-hidden rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md">
<div className="pointer-events-auto fixed top-20 right-4 z-50 flex w-82 flex-col overflow-hidden rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md">
{/* Header */}
<div className="flex items-center justify-between gap-2 border-b p-3">
<div className="flex items-center gap-2 min-w-0">
@@ -59,6 +98,36 @@ export function WindowPanel() {
{/* Content */}
<div className="flex-1 overflow-y-auto p-3 space-y-4">
{/* Position */}
<div className="space-y-2">
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Position
</label>
<div className="grid grid-cols-2 gap-2">
<NumberInput
label="X"
value={Math.round(node.position[0] * 100) / 100}
onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
precision={2}
/>
<NumberInput
label="Y"
value={Math.round(node.position[1] * 100) / 100}
onChange={(v) => handleUpdate({ position: [node.position[0], v, node.position[2]] })}
precision={2}
/>
</div>
<button
type="button"
className="w-full flex items-center justify-center gap-1.5 rounded border border-border px-2 py-1.5 text-xs hover:bg-accent cursor-pointer"
onClick={handleFlip}
>
<FlipHorizontal2 className="h-3.5 w-3.5" />
Flip Side
</button>
</div>
{/* Dimensions */}
<div className="space-y-2">
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
@@ -68,7 +137,7 @@ export function WindowPanel() {
<div className="flex items-center gap-1.5">
<NumberInput
label="Width"
value={Math.round(node.width * 1000) / 1000}
value={Math.round(node.width * 100) / 100}
onChange={(v) => handleUpdate({ width: v })}
min={0.2}
precision={2}
@@ -79,7 +148,7 @@ export function WindowPanel() {
<div className="flex items-center gap-1.5">
<NumberInput
label="Height"
value={Math.round(node.height * 1000) / 1000}
value={Math.round(node.height * 100) / 100}
onChange={(v) => handleUpdate({ height: v })}
min={0.2}
precision={2}
@@ -103,6 +172,7 @@ export function WindowPanel() {
onChange={(v) => handleUpdate({ frameThickness: v })}
min={0.01}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
@@ -114,6 +184,7 @@ export function WindowPanel() {
onChange={(v) => handleUpdate({ frameDepth: v })}
min={0.01}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
@@ -127,47 +198,98 @@ export function WindowPanel() {
Grid
</label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-1.5">
<NumberInput
label="Columns"
value={columns}
value={numCols}
onChange={(v) => {
const n = Math.max(1, Math.min(8, Math.round(v)))
handleUpdate({ columnRatios: Array(n).fill(1) })
handleUpdate({ columnRatios: Array(n).fill(1 / n) })
}}
min={1}
max={8}
precision={0}
className="flex-1"
step={1}
/>
</div>
<div className="flex items-center gap-1.5">
<NumberInput
label="Rows"
value={rows}
value={numRows}
onChange={(v) => {
const n = Math.max(1, Math.min(8, Math.round(v)))
handleUpdate({ rowRatios: Array(n).fill(1) })
handleUpdate({ rowRatios: Array(n).fill(1 / n) })
}}
min={1}
max={8}
precision={0}
className="flex-1"
step={1}
/>
</div>
{/* Column ratios */}
{numCols > 1 && (
<div className="space-y-1">
<span className="text-muted-foreground text-xs">Column widths</span>
{normCols.map((ratio, i) => (
<div key={i} className="flex items-center gap-1.5">
<NumberInput
label={`C${i + 1}`}
value={Math.round(ratio * 100 * 10) / 10}
onChange={(v) => setColumnRatio(i, v / 100)}
min={5}
max={95}
precision={1}
step={1}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">%</span>
</div>
{(columns > 1 || rows > 1) && (
))}
<div className="flex items-center gap-1.5">
<NumberInput
label="Divider thickness"
value={Math.round(node.dividerThickness * 1000) / 1000}
onChange={(v) => handleUpdate({ dividerThickness: v })}
label="Col divider"
value={Math.round((node.columnDividerThickness ?? 0.03) * 1000) / 1000}
onChange={(v) => handleUpdate({ columnDividerThickness: v })}
min={0.005}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
</div>
</div>
)}
{/* Row ratios */}
{numRows > 1 && (
<div className="space-y-1">
<span className="text-muted-foreground text-xs">Row heights</span>
{normRows.map((ratio, i) => (
<div key={i} className="flex items-center gap-1.5">
<NumberInput
label={`R${i + 1}`}
value={Math.round(ratio * 100 * 10) / 10}
onChange={(v) => setRowRatio(i, v / 100)}
min={5}
max={95}
precision={1}
step={1}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">%</span>
</div>
))}
<div className="flex items-center gap-1.5">
<NumberInput
label="Row divider"
value={Math.round((node.rowDividerThickness ?? 0.03) * 1000) / 1000}
onChange={(v) => handleUpdate({ rowDividerThickness: v })}
min={0.005}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
</div>
</div>
)}
</div>
@@ -191,6 +313,7 @@ export function WindowPanel() {
onChange={(v) => handleUpdate({ sillDepth: v })}
min={0.01}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
@@ -202,6 +325,7 @@ export function WindowPanel() {
onChange={(v) => handleUpdate({ sillThickness: v })}
min={0.005}
precision={3}
step={0.01}
className="flex-1"
/>
<span className="text-muted-foreground text-xs shrink-0">m</span>
@@ -10,6 +10,7 @@ interface NumberInputProps {
min?: number
max?: number
precision?: number
step?: number
className?: string
}
@@ -20,6 +21,7 @@ export function NumberInput({
min,
max,
precision = 2,
step = 0.1,
className = '',
}: NumberInputProps) {
const [isEditing, setIsEditing] = useState(false)
@@ -55,14 +57,14 @@ export function NumberInput({
const deltaX = moveEvent.clientX - startXRef.current
// Determine step size based on modifier keys
let step = 0.1 // Default
let dragStep = step // Default from prop
if (moveEvent.shiftKey) {
step = 1.0 // Coarse
dragStep = step * 10 // Coarse
} else if (moveEvent.altKey) {
step = 0.01 // Fine
dragStep = step * 0.1 // Fine
}
const deltaValue = deltaX * step
const deltaValue = deltaX * dragStep
const newValue = clamp(startValueRef.current + deltaValue)
const newFinalValue = Number.parseFloat(newValue.toFixed(precision))
+2 -1
View File
@@ -28,7 +28,8 @@ export const WindowNode = BaseNode.extend({
// [1] = single pane (no division)
columnRatios: z.array(z.number()).default([1]),
rowRatios: z.array(z.number()).default([1]),
dividerThickness: z.number().default(0.03),
columnDividerThickness: z.number().default(0.03),
rowDividerThickness: z.number().default(0.03),
// Sill
sill: z.boolean().default(true),
@@ -85,7 +85,7 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
const {
width, height, frameDepth, frameThickness,
columnRatios, rowRatios, dividerThickness,
columnRatios, rowRatios, columnDividerThickness, rowDividerThickness,
sill, sillDepth, sillThickness,
} = node
@@ -104,8 +104,8 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
const numCols = columnRatios.length
const numRows = rowRatios.length
const usableW = innerW - (numCols - 1) * dividerThickness
const usableH = innerH - (numRows - 1) * dividerThickness
const usableW = innerW - (numCols - 1) * columnDividerThickness
const usableH = innerH - (numRows - 1) * rowDividerThickness
const colSum = columnRatios.reduce((a, b) => a + b, 0)
const rowSum = rowRatios.reduce((a, b) => a + b, 0)
@@ -118,35 +118,35 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
for (let c = 0; c < numCols; c++) {
colXCenters.push(cx + colWidths[c]! / 2)
cx += colWidths[c]!
if (c < numCols - 1) cx += dividerThickness
if (c < numCols - 1) cx += columnDividerThickness
}
// Compute row y-centers starting from bottom edge of inner area
// Compute row y-centers starting from top edge of inner area (R1 = top)
const rowYCenters: number[] = []
let cy = -innerH / 2
let cy = innerH / 2
for (let r = 0; r < numRows; r++) {
rowYCenters.push(cy + rowHeights[r]! / 2)
cy += rowHeights[r]!
if (r < numRows - 1) cy += dividerThickness
rowYCenters.push(cy - rowHeights[r]! / 2)
cy -= rowHeights[r]!
if (r < numRows - 1) cy -= rowDividerThickness
}
// Column dividers — full inner height
cx = -innerW / 2
for (let c = 0; c < numCols - 1; c++) {
cx += colWidths[c]!
addBox(mesh, frameMaterial, dividerThickness, innerH, frameDepth, cx + dividerThickness / 2, 0, 0)
cx += dividerThickness
addBox(mesh, frameMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0)
cx += columnDividerThickness
}
// Row dividers — per column width, so they don't overlap column dividers
cy = -innerH / 2
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
cy = innerH / 2
for (let r = 0; r < numRows - 1; r++) {
cy += rowHeights[r]!
const divY = cy + dividerThickness / 2
cy -= rowHeights[r]!
const divY = cy - rowDividerThickness / 2
for (let c = 0; c < numCols; c++) {
addBox(mesh, frameMaterial, colWidths[c]!, dividerThickness, frameDepth, colXCenters[c]!, divY, 0)
addBox(mesh, frameMaterial, colWidths[c]!, rowDividerThickness, frameDepth, colXCenters[c]!, divY, 0)
}
cy += dividerThickness
cy -= rowDividerThickness
}
// Glass panes