feat: editor improvements and collapsible sidebars
Add collapse/expand toggle for the AppShell navigation sidebar and the editor properties panel (both persisted to localStorage). Bundles other in-progress editor work including position anchors, outlet sizing, PBR textures, window slope/frame depth, curtain metadata, and various 2D/3D rendering tweaks.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Group, Circle, Line, Rect, Text } from 'react-konva';
|
||||
import type { ProjectedElectrical } from '../utils/projectionMapping';
|
||||
import { projectionToPixel } from '../utils/projectionMapping';
|
||||
import { DEFAULT_OUTLET_WIDTH, DEFAULT_OUTLET_HEIGHT } from '@house-plan-maker/shared';
|
||||
|
||||
interface ProjectionElectricalProps {
|
||||
readonly projected: ProjectedElectrical;
|
||||
@@ -11,6 +12,10 @@ interface ProjectionElectricalProps {
|
||||
readonly isDragging?: boolean;
|
||||
readonly dragFromFloor?: number;
|
||||
readonly dragAlongWall?: number;
|
||||
/** Physical width of a single outlet face plate (meters). */
|
||||
readonly outletWidth?: number;
|
||||
/** Physical height of a single outlet face plate (meters). */
|
||||
readonly outletHeight?: number;
|
||||
readonly onClick: () => void;
|
||||
readonly onDragStart?: (itemId: string, evt: MouseEvent) => void;
|
||||
}
|
||||
@@ -27,6 +32,8 @@ export function ProjectionElectrical({
|
||||
isDragging = false,
|
||||
dragFromFloor,
|
||||
dragAlongWall,
|
||||
outletWidth = DEFAULT_OUTLET_WIDTH,
|
||||
outletHeight = DEFAULT_OUTLET_HEIGHT,
|
||||
onClick,
|
||||
onDragStart,
|
||||
}: ProjectionElectricalProps) {
|
||||
@@ -70,29 +77,84 @@ export function ProjectionElectrical({
|
||||
fill="transparent"
|
||||
/>
|
||||
)}
|
||||
{item.type === 'OUTLET' && (
|
||||
<>
|
||||
{/* IEC outlet symbol: circle with two horizontal lines */}
|
||||
<Circle
|
||||
x={center.x}
|
||||
y={center.y}
|
||||
radius={half}
|
||||
fill={fillColor}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<Line
|
||||
points={[center.x - 3, center.y - 2, center.x + 3, center.y - 2]}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<Line
|
||||
points={[center.x - 3, center.y + 2, center.x + 3, center.y + 2]}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{item.type === 'OUTLET' && (() => {
|
||||
const safeCount = Math.max(1, Math.round(item.count));
|
||||
// Convert physical outlet dims to projection-pixel dims.
|
||||
const wPx = outletWidth * scale;
|
||||
const hPx = outletHeight * scale;
|
||||
// Anchor offset to bounding-box center, in projection pixels.
|
||||
// Horizontal axis = along-wall (positive right), vertical axis = up the wall.
|
||||
// In screen coords +y is down, so vertical='top' anchor means center is BELOW (positive y).
|
||||
//
|
||||
// When the projection axis is flipped (the canonical direction
|
||||
// runs opposite to the wall's stored start→end), we mirror the
|
||||
// horizontal anchor so "left" still refers to the same physical
|
||||
// side of the wall in both 3D and projection views. Without this
|
||||
// an outlet anchored "left" on a flipped wall would appear on
|
||||
// opposite sides of the two views.
|
||||
const anchor = item.positionAnchor;
|
||||
const mirroredHorizontal = projected.axisFlipped
|
||||
? anchor.horizontal === 'left'
|
||||
? 'right'
|
||||
: anchor.horizontal === 'right'
|
||||
? 'left'
|
||||
: 'middle'
|
||||
: anchor.horizontal;
|
||||
const totalW = safeCount * wPx;
|
||||
const offX =
|
||||
mirroredHorizontal === 'left' ? totalW / 2 : mirroredHorizontal === 'right' ? -totalW / 2 : 0;
|
||||
const offY =
|
||||
anchor.vertical === 'top' ? hPx / 2 : anchor.vertical === 'bottom' ? -hPx / 2 : 0;
|
||||
const cx = center.x + offX;
|
||||
const cy = center.y + offY;
|
||||
const left = cx - totalW / 2;
|
||||
const top = cy - hPx / 2;
|
||||
const cellMin = Math.min(wPx, hPx);
|
||||
const faceR = cellMin * 0.32;
|
||||
const prongL = cellMin * 0.18;
|
||||
const prongG = cellMin * 0.12;
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: safeCount }).map((_, i) => {
|
||||
const cellLeft = left + i * wPx;
|
||||
const cellCx = cellLeft + wPx / 2;
|
||||
const cellCy = top + hPx / 2;
|
||||
return (
|
||||
<Group key={i}>
|
||||
<Rect
|
||||
x={cellLeft}
|
||||
y={top}
|
||||
width={wPx}
|
||||
height={hPx}
|
||||
cornerRadius={Math.max(1, cellMin * 0.12)}
|
||||
fill={fillColor}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.25}
|
||||
/>
|
||||
<Circle
|
||||
x={cellCx}
|
||||
y={cellCy}
|
||||
radius={faceR}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.25}
|
||||
fill="transparent"
|
||||
/>
|
||||
<Line
|
||||
points={[cellCx - prongG, cellCy - prongL, cellCx - prongG, cellCy + prongL]}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.25}
|
||||
/>
|
||||
<Line
|
||||
points={[cellCx + prongG, cellCy - prongL, cellCx + prongG, cellCy + prongL]}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.25}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{item.type === 'SWITCH' && (
|
||||
<>
|
||||
{/* IEC switch symbol: circle with diagonal line */}
|
||||
@@ -157,12 +219,21 @@ export function ProjectionElectrical({
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
{/* Type label below symbol */}
|
||||
{/* Type label below symbol — uses the user's custom label if set,
|
||||
otherwise falls back to the short type code. */}
|
||||
<Text
|
||||
x={center.x - 20}
|
||||
x={center.x - 30}
|
||||
y={center.y + half + 2}
|
||||
width={40}
|
||||
text={item.type === 'OUTLET' ? 'OUT' : item.type === 'SWITCH' ? 'SW' : 'WL'}
|
||||
width={60}
|
||||
text={
|
||||
item.label && item.label.trim().length > 0
|
||||
? item.label
|
||||
: item.type === 'OUTLET'
|
||||
? 'OUT'
|
||||
: item.type === 'SWITCH'
|
||||
? 'SW'
|
||||
: 'WL'
|
||||
}
|
||||
align="center"
|
||||
fontSize={8}
|
||||
fill="#94a3b8"
|
||||
|
||||
Reference in New Issue
Block a user