nice progress on doors
This commit is contained in:
@@ -91,6 +91,28 @@ export function DoorPanel() {
|
|||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [node, setMovingNode, setSelection])
|
}, [node, setMovingNode, setSelection])
|
||||||
|
|
||||||
|
const setSegmentColumnRatio = (segIdx: number, colIdx: number, newVal: number) => {
|
||||||
|
const seg = node!.segments[segIdx]!
|
||||||
|
const normRatios = (() => {
|
||||||
|
const sum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
||||||
|
return seg.columnRatios.map(r => r / sum)
|
||||||
|
})()
|
||||||
|
const numCols = normRatios.length
|
||||||
|
const clamped = Math.max(0.05, Math.min(0.95, newVal))
|
||||||
|
const neighborIdx = colIdx < numCols - 1 ? colIdx + 1 : colIdx - 1
|
||||||
|
const delta = clamped - normRatios[colIdx]!
|
||||||
|
const neighborVal = Math.max(0.05, normRatios[neighborIdx]! - delta)
|
||||||
|
const newRatios = normRatios.map((v, i) => {
|
||||||
|
if (i === colIdx) return clamped
|
||||||
|
if (i === neighborIdx) return neighborVal
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
const updated = node!.segments.map((s, idx) =>
|
||||||
|
idx === segIdx ? { ...s, columnRatios: newRatios } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}
|
||||||
|
|
||||||
if (!node || node.type !== 'door' || selectedIds.length !== 1) return null
|
if (!node || node.type !== 'door' || selectedIds.length !== 1) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,7 +181,7 @@ export function DoorPanel() {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
label="Height"
|
label="Height"
|
||||||
value={Math.round(node.height * 100) / 100}
|
value={Math.round(node.height * 100) / 100}
|
||||||
onChange={(v) => handleUpdate({ height: v })}
|
onChange={(v) => handleUpdate({ height: v, position: [node.position[0], v / 2, node.position[2]] })}
|
||||||
min={1.0}
|
min={1.0}
|
||||||
precision={2}
|
precision={2}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -202,6 +224,39 @@ export function DoorPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Content Padding */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Content Padding
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Horizontal"
|
||||||
|
value={Math.round(node.contentPadding[0] * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] })}
|
||||||
|
min={0}
|
||||||
|
precision={3}
|
||||||
|
step={0.005}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Vertical"
|
||||||
|
value={Math.round(node.contentPadding[1] * 1000) / 1000}
|
||||||
|
onChange={(v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] })}
|
||||||
|
min={0}
|
||||||
|
precision={3}
|
||||||
|
step={0.005}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Swing */}
|
{/* Swing */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
@@ -368,87 +423,150 @@ export function DoorPanel() {
|
|||||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
Leaf segments (top → bottom)
|
Leaf segments (top → bottom)
|
||||||
</label>
|
</label>
|
||||||
{node.segments.map((seg, i) => (
|
{node.segments.map((seg, i) => {
|
||||||
<div key={i} className="rounded border border-border p-2 space-y-2">
|
const numCols = seg.columnRatios.length
|
||||||
<div className="flex items-center justify-between gap-2">
|
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
||||||
<span className="text-xs text-muted-foreground">Segment {i + 1}</span>
|
const normCols = seg.columnRatios.map(r => r / colSum)
|
||||||
<div className="flex gap-1">
|
return (
|
||||||
{(['panel', 'glass', 'empty'] as const).map((t) => (
|
<div key={i} className="rounded border border-border p-2 space-y-2">
|
||||||
<button
|
<div className="flex items-center justify-between gap-2">
|
||||||
key={t}
|
<span className="text-xs text-muted-foreground">Segment {i + 1}</span>
|
||||||
type="button"
|
<div className="flex gap-1">
|
||||||
onClick={() => {
|
{(['panel', 'glass', 'empty'] as const).map((t) => (
|
||||||
const updated = node.segments.map((s, idx) =>
|
<button
|
||||||
idx === i ? { ...s, type: t } : s,
|
key={t}
|
||||||
)
|
type="button"
|
||||||
handleUpdate({ segments: updated })
|
onClick={() => {
|
||||||
}}
|
const updated = node.segments.map((s, idx) =>
|
||||||
className={`rounded border px-1.5 py-0.5 text-xs cursor-pointer transition-colors ${
|
idx === i ? { ...s, type: t } : s,
|
||||||
seg.type === t
|
)
|
||||||
? 'border-primary bg-primary text-primary-foreground'
|
handleUpdate({ segments: updated })
|
||||||
: 'border-border hover:bg-accent'
|
}}
|
||||||
}`}
|
className={`rounded border px-1.5 py-0.5 text-xs cursor-pointer transition-colors ${
|
||||||
>
|
seg.type === t
|
||||||
{t}
|
? 'border-primary bg-primary text-primary-foreground'
|
||||||
</button>
|
: 'border-border hover:bg-accent'
|
||||||
))}
|
}`}
|
||||||
</div>
|
>
|
||||||
</div>
|
{t}
|
||||||
<div className="flex items-center gap-1.5">
|
</button>
|
||||||
<NumberInput
|
))}
|
||||||
label="Height ratio"
|
|
||||||
value={Math.round(seg.heightRatio * 100) / 100}
|
|
||||||
onChange={(v) => {
|
|
||||||
const updated = node.segments.map((s, idx) =>
|
|
||||||
idx === i ? { ...s, heightRatio: Math.max(0.05, v) } : s,
|
|
||||||
)
|
|
||||||
handleUpdate({ segments: updated })
|
|
||||||
}}
|
|
||||||
min={0.05}
|
|
||||||
precision={2}
|
|
||||||
step={0.05}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{seg.type === 'panel' && (
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<NumberInput
|
|
||||||
label="Inset"
|
|
||||||
value={Math.round(seg.panelInset * 1000) / 1000}
|
|
||||||
onChange={(v) => {
|
|
||||||
const updated = node.segments.map((s, idx) =>
|
|
||||||
idx === i ? { ...s, panelInset: v } : s,
|
|
||||||
)
|
|
||||||
handleUpdate({ segments: updated })
|
|
||||||
}}
|
|
||||||
min={0.005}
|
|
||||||
precision={3}
|
|
||||||
step={0.005}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<NumberInput
|
|
||||||
label="Depth"
|
|
||||||
value={Math.round(seg.panelDepth * 1000) / 1000}
|
|
||||||
onChange={(v) => {
|
|
||||||
const updated = node.segments.map((s, idx) =>
|
|
||||||
idx === i ? { ...s, panelDepth: v } : s,
|
|
||||||
)
|
|
||||||
handleUpdate({ segments: updated })
|
|
||||||
}}
|
|
||||||
precision={3}
|
|
||||||
step={0.005}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-1.5">
|
||||||
</div>
|
<NumberInput
|
||||||
))}
|
label="Height ratio"
|
||||||
|
value={Math.round(seg.heightRatio * 100) / 100}
|
||||||
|
onChange={(v) => {
|
||||||
|
const updated = node.segments.map((s, idx) =>
|
||||||
|
idx === i ? { ...s, heightRatio: Math.max(0.05, v) } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}}
|
||||||
|
min={0.05}
|
||||||
|
precision={2}
|
||||||
|
step={0.05}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Columns */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Columns</span>
|
||||||
|
<NumberInput
|
||||||
|
label=""
|
||||||
|
value={numCols}
|
||||||
|
onChange={(v) => {
|
||||||
|
const n = Math.max(1, Math.min(8, Math.round(v)))
|
||||||
|
const updated = node.segments.map((s, idx) =>
|
||||||
|
idx === i ? { ...s, columnRatios: Array(n).fill(1 / n) } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
max={8}
|
||||||
|
precision={0}
|
||||||
|
step={1}
|
||||||
|
className="w-16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{numCols > 1 && (
|
||||||
|
<div className="space-y-1 pl-1">
|
||||||
|
{normCols.map((ratio, ci) => (
|
||||||
|
<div key={ci} className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label={`C${ci + 1}`}
|
||||||
|
value={Math.round(ratio * 100 * 10) / 10}
|
||||||
|
onChange={(v) => setSegmentColumnRatio(i, ci, 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="Divider"
|
||||||
|
value={Math.round(seg.dividerThickness * 1000) / 1000}
|
||||||
|
onChange={(v) => {
|
||||||
|
const updated = node.segments.map((s, idx) =>
|
||||||
|
idx === i ? { ...s, dividerThickness: v } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}}
|
||||||
|
min={0.005}
|
||||||
|
precision={3}
|
||||||
|
step={0.005}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{seg.type === 'panel' && (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Inset"
|
||||||
|
value={Math.round(seg.panelInset * 1000) / 1000}
|
||||||
|
onChange={(v) => {
|
||||||
|
const updated = node.segments.map((s, idx) =>
|
||||||
|
idx === i ? { ...s, panelInset: v } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}}
|
||||||
|
min={0.005}
|
||||||
|
precision={3}
|
||||||
|
step={0.005}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<NumberInput
|
||||||
|
label="Depth"
|
||||||
|
value={Math.round(seg.panelDepth * 1000) / 1000}
|
||||||
|
onChange={(v) => {
|
||||||
|
const updated = node.segments.map((s, idx) =>
|
||||||
|
idx === i ? { ...s, panelDepth: v } : s,
|
||||||
|
)
|
||||||
|
handleUpdate({ segments: updated })
|
||||||
|
}}
|
||||||
|
precision={3}
|
||||||
|
step={0.005}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs shrink-0">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export const DoorNode = BaseNode.extend({
|
|||||||
handleHeight: z.number().default(1.05),
|
handleHeight: z.number().default(1.05),
|
||||||
handleSide: z.enum(['left', 'right']).default('right'),
|
handleSide: z.enum(['left', 'right']).default('right'),
|
||||||
|
|
||||||
|
// Leaf inner margin — space between leaf edge and segment content area [x, y]
|
||||||
|
contentPadding: z.tuple([z.number(), z.number()]).default([0.04, 0.04]),
|
||||||
|
|
||||||
// Emergency / commercial hardware
|
// Emergency / commercial hardware
|
||||||
doorCloser: z.boolean().default(false),
|
doorCloser: z.boolean().default(false),
|
||||||
panicBar: z.boolean().default(false),
|
panicBar: z.boolean().default(false),
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
const {
|
const {
|
||||||
width, height, frameThickness, frameDepth, threshold, thresholdHeight,
|
width, height, frameThickness, frameDepth, threshold, thresholdHeight,
|
||||||
segments, handle, handleHeight, handleSide,
|
segments, handle, handleHeight, handleSide,
|
||||||
doorCloser, panicBar, panicBarHeight,
|
doorCloser, panicBar, panicBarHeight, contentPadding,
|
||||||
} = node
|
} = node
|
||||||
|
|
||||||
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
|
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
|
||||||
@@ -139,31 +139,49 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
// Head (top bar) — full width
|
// Head (top bar) — full width
|
||||||
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0)
|
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0)
|
||||||
|
|
||||||
// ── Threshold ──
|
// ── Threshold (inside the frame) ──
|
||||||
if (threshold) {
|
if (threshold) {
|
||||||
addBox(mesh, thresholdMaterial, width, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0)
|
addBox(mesh, thresholdMaterial, leafW, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Door leaf — full backing ──
|
// ── Leaf — contentPadding border strips (no full backing; glass areas are open) ──
|
||||||
addBox(mesh, leafMaterial, leafW, leafH, leafDepth, 0, leafCenterY, 0)
|
const cpX = contentPadding[0]
|
||||||
|
const cpY = contentPadding[1]
|
||||||
|
if (cpY > 0) {
|
||||||
|
// Top strip
|
||||||
|
addBox(mesh, leafMaterial, leafW, cpY, leafDepth, 0, leafCenterY + leafH / 2 - cpY / 2, 0)
|
||||||
|
// Bottom strip
|
||||||
|
addBox(mesh, leafMaterial, leafW, cpY, leafDepth, 0, leafCenterY - leafH / 2 + cpY / 2, 0)
|
||||||
|
}
|
||||||
|
if (cpX > 0) {
|
||||||
|
const innerH = leafH - 2 * cpY
|
||||||
|
// Left strip
|
||||||
|
addBox(mesh, leafMaterial, cpX, innerH, leafDepth, -leafW / 2 + cpX / 2, leafCenterY, 0)
|
||||||
|
// Right strip
|
||||||
|
addBox(mesh, leafMaterial, cpX, innerH, leafDepth, leafW / 2 - cpX / 2, leafCenterY, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Segments (stacked top to bottom within leaf area) ──
|
// Content area inside padding
|
||||||
|
const contentW = leafW - 2 * cpX
|
||||||
|
const contentH = leafH - 2 * cpY
|
||||||
|
|
||||||
|
// ── Segments (stacked top to bottom within content area) ──
|
||||||
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0)
|
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0)
|
||||||
const leafTop = leafCenterY + leafH / 2
|
const contentTop = leafCenterY + contentH / 2
|
||||||
|
|
||||||
let segY = leafTop
|
let segY = contentTop
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
const segH = (seg.heightRatio / totalRatio) * leafH
|
const segH = (seg.heightRatio / totalRatio) * contentH
|
||||||
const segCenterY = segY - segH / 2
|
const segCenterY = segY - segH / 2
|
||||||
|
|
||||||
const numCols = seg.columnRatios.length
|
const numCols = seg.columnRatios.length
|
||||||
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
||||||
const usableW = leafW - (numCols - 1) * seg.dividerThickness
|
const usableW = contentW - (numCols - 1) * seg.dividerThickness
|
||||||
const colWidths = seg.columnRatios.map(r => (r / colSum) * usableW)
|
const colWidths = seg.columnRatios.map(r => (r / colSum) * usableW)
|
||||||
|
|
||||||
// Column x-centers
|
// Column x-centers (relative to mesh center)
|
||||||
const colXCenters: number[] = []
|
const colXCenters: number[] = []
|
||||||
let cx = -leafW / 2
|
let cx = -contentW / 2
|
||||||
for (let c = 0; c < numCols; c++) {
|
for (let c = 0; c < numCols; c++) {
|
||||||
colXCenters.push(cx + colWidths[c]! / 2)
|
colXCenters.push(cx + colWidths[c]! / 2)
|
||||||
cx += colWidths[c]!
|
cx += colWidths[c]!
|
||||||
@@ -171,7 +189,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Column dividers within this segment
|
// Column dividers within this segment
|
||||||
cx = -leafW / 2
|
cx = -contentW / 2
|
||||||
for (let c = 0; c < numCols - 1; c++) {
|
for (let c = 0; c < numCols - 1; c++) {
|
||||||
cx += colWidths[c]!
|
cx += colWidths[c]!
|
||||||
addBox(mesh, leafMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0)
|
addBox(mesh, leafMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0)
|
||||||
@@ -184,9 +202,13 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
const colX = colXCenters[c]!
|
const colX = colXCenters[c]!
|
||||||
|
|
||||||
if (seg.type === 'glass') {
|
if (seg.type === 'glass') {
|
||||||
|
// Glass only — no opaque backing so it's truly transparent
|
||||||
const glassDepth = Math.max(0.004, leafDepth * 0.15)
|
const glassDepth = Math.max(0.004, leafDepth * 0.15)
|
||||||
addBox(mesh, glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0)
|
addBox(mesh, glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0)
|
||||||
} else if (seg.type === 'panel') {
|
} else if (seg.type === 'panel') {
|
||||||
|
// Opaque leaf backing for this column
|
||||||
|
addBox(mesh, leafMaterial, colW, segH, leafDepth, colX, segCenterY, 0)
|
||||||
|
// Raised panel detail
|
||||||
const panelW = colW - 2 * seg.panelInset
|
const panelW = colW - 2 * seg.panelInset
|
||||||
const panelH = segH - 2 * seg.panelInset
|
const panelH = segH - 2 * seg.panelInset
|
||||||
if (panelW > 0.01 && panelH > 0.01) {
|
if (panelW > 0.01 && panelH > 0.01) {
|
||||||
@@ -194,8 +216,10 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
const panelZ = leafDepth / 2 + effectiveDepth / 2
|
const panelZ = leafDepth / 2 + effectiveDepth / 2
|
||||||
addBox(mesh, panelMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ)
|
addBox(mesh, panelMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 'empty' — opaque backing, no detail
|
||||||
|
addBox(mesh, leafMaterial, colW, segH, leafDepth, colX, segCenterY, 0)
|
||||||
}
|
}
|
||||||
// 'empty' → leaf backing is already there, nothing extra
|
|
||||||
}
|
}
|
||||||
|
|
||||||
segY -= segH
|
segY -= segH
|
||||||
|
|||||||
Reference in New Issue
Block a user