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 { memo, useMemo } from 'react';
|
||||
import { Layer, Text, Rect, Group, Line } from 'react-konva';
|
||||
import { Text, Rect, Group, Line } from 'react-konva';
|
||||
import type { Point, Annotation, ElectricalItem, FurnitureItem } from '@house-plan-maker/shared';
|
||||
import { rotatedAnchorOffsetToCenter } from '@house-plan-maker/shared';
|
||||
|
||||
interface AnnotationLayerProps {
|
||||
readonly annotations: readonly Annotation[];
|
||||
@@ -17,9 +18,19 @@ interface AnnotationLayerProps {
|
||||
|
||||
const DEFAULT_FONT_SIZE = 14;
|
||||
const DEFAULT_COLOR = '#333333';
|
||||
const LINK_COLOR = '#2563eb';
|
||||
const SELECTED_COLOR = '#4c6ef5';
|
||||
const SELECTION_PADDING = 4;
|
||||
|
||||
// Match plain http(s) URLs only — anything else stays a regular annotation.
|
||||
// Anchored to start/end so a label like "see http://x" isn't treated as a
|
||||
// link (we want the whole text to be the URL).
|
||||
const URL_PATTERN = /^https?:\/\/\S+$/i;
|
||||
|
||||
function isUrlAnnotation(text: string): boolean {
|
||||
return URL_PATTERN.test(text.trim());
|
||||
}
|
||||
|
||||
function toScreen(point: Point, zoom: number, panOffset: Point): { x: number; y: number } {
|
||||
return {
|
||||
x: point.x * zoom + panOffset.x,
|
||||
@@ -46,7 +57,13 @@ export const AnnotationLayer = memo(function AnnotationLayer({
|
||||
map.set(item.id, { x: item.x, y: item.y });
|
||||
}
|
||||
for (const item of furnitureItems) {
|
||||
map.set(item.id, { x: item.x + item.width / 2, y: item.y + item.depth / 2 });
|
||||
const offset = rotatedAnchorOffsetToCenter(
|
||||
item.positionAnchor,
|
||||
item.width,
|
||||
item.depth,
|
||||
item.rotation,
|
||||
);
|
||||
map.set(item.id, { x: item.x + offset.dx, y: item.y + offset.dy });
|
||||
}
|
||||
return map;
|
||||
}, [electricalItems, furnitureItems]);
|
||||
@@ -57,7 +74,7 @@ export const AnnotationLayer = memo(function AnnotationLayer({
|
||||
}, [annotations, visible]);
|
||||
|
||||
return (
|
||||
<Layer visible={visible}>
|
||||
<Group visible={visible}>
|
||||
{renderedAnnotations.map((annotation) => {
|
||||
// Resolve position: if attached, offset from parent item
|
||||
let worldX = annotation.x;
|
||||
@@ -76,7 +93,15 @@ export const AnnotationLayer = memo(function AnnotationLayer({
|
||||
const screen = toScreen({ x: worldX, y: worldY }, zoom, panOffset);
|
||||
const isSelected = selectedIds.has(annotation.id);
|
||||
const fontSize = annotation.fontSize ?? DEFAULT_FONT_SIZE;
|
||||
const color = isSelected ? SELECTED_COLOR : (annotation.color ?? DEFAULT_COLOR);
|
||||
const isLink = isUrlAnnotation(annotation.text);
|
||||
// Link annotations get a distinctive blue tint when not selected so
|
||||
// users can spot them; selection still wins to keep the affordance
|
||||
// consistent with non-link annotations.
|
||||
const color = isSelected
|
||||
? SELECTED_COLOR
|
||||
: isLink
|
||||
? (annotation.color ?? LINK_COLOR)
|
||||
: (annotation.color ?? DEFAULT_COLOR);
|
||||
|
||||
return (
|
||||
<Group key={annotation.id}>
|
||||
@@ -108,7 +133,19 @@ export const AnnotationLayer = memo(function AnnotationLayer({
|
||||
}
|
||||
onDragEnd?.(annotation.id, newX, newY);
|
||||
}}
|
||||
onClick={() => onSelect?.(annotation.id)}
|
||||
onClick={(e) => {
|
||||
// Ctrl/Cmd-click on a URL annotation opens it in a new tab.
|
||||
// We swallow the event so it doesn't also trigger selection
|
||||
// or upstream stage handlers (which would deselect the link
|
||||
// immediately on focus loss). Plain clicks fall through to
|
||||
// the regular select handler.
|
||||
if (isLink && (e.evt.ctrlKey || e.evt.metaKey)) {
|
||||
e.cancelBubble = true;
|
||||
window.open(annotation.text.trim(), '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
onSelect?.(annotation.id);
|
||||
}}
|
||||
onDblClick={() => onDoubleClick?.(annotation.id)}
|
||||
>
|
||||
{/* Background */}
|
||||
@@ -143,6 +180,6 @@ export const AnnotationLayer = memo(function AnnotationLayer({
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Layer>
|
||||
</Group>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user