T24: Use shared types, add 300ms debounce on Yjs updates in PropertiesPanel
This commit is contained in:
@@ -1,39 +1,8 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import type { CADLayer, CADProperties, CADElement } from '../../types/cad.types';
|
||||||
import './PropertiesPanel.css';
|
import './PropertiesPanel.css';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types (local-only, not duplicated from cad.types.ts) ─────────────────────
|
||||||
|
|
||||||
interface CADLayer {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
visible: boolean;
|
|
||||||
locked: boolean;
|
|
||||||
color?: string;
|
|
||||||
lineType?: string;
|
|
||||||
transparency?: number;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CADProperties {
|
|
||||||
fill?: string;
|
|
||||||
stroke?: string;
|
|
||||||
strokeWidth?: number;
|
|
||||||
rotation?: number;
|
|
||||||
lineType?: 'solid' | 'dashed' | 'dotted';
|
|
||||||
radius?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CADElement {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
layerId: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
properties: CADProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface YjsDocumentLike {
|
interface YjsDocumentLike {
|
||||||
layers: {
|
layers: {
|
||||||
@@ -112,6 +81,18 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef
|
|||||||
const [fabricObject, setFabricObject] = useState<FabricObjectLike | null>(null);
|
const [fabricObject, setFabricObject] = useState<FabricObjectLike | null>(null);
|
||||||
const fabricCanvasRef = useRef<FabricCanvasLike | null>(null);
|
const fabricCanvasRef = useRef<FabricCanvasLike | null>(null);
|
||||||
|
|
||||||
|
// Debounce timer for Yjs updates (300ms) — no new dependency needed
|
||||||
|
const yjsDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Clear debounce timer on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (yjsDebounceRef.current) {
|
||||||
|
clearTimeout(yjsDebounceRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ── Sync layers from YjsDocument ──────────────────────────────────────────
|
// ── Sync layers from YjsDocument ──────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const yjsDoc = yjsDocRef?.current;
|
const yjsDoc = yjsDocRef?.current;
|
||||||
@@ -215,19 +196,26 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef
|
|||||||
};
|
};
|
||||||
}, [canvasRef, yjsDocRef]);
|
}, [canvasRef, yjsDocRef]);
|
||||||
|
|
||||||
// ── Update handlers (Panel → Canvas + YjsDocument) ─────────────────────────
|
// ── Update handlers (Panel → Canvas + YjsDocument) ──────────────────────────
|
||||||
|
|
||||||
const updateFabricAndYjs = useCallback(
|
const updateFabricAndYjs = useCallback(
|
||||||
(updates: Partial<CADElement> & { properties?: Partial<CADProperties> }) => {
|
(updates: Partial<CADElement> & { properties?: Partial<CADProperties> }) => {
|
||||||
if (!selectedElement) return;
|
if (!selectedElement) return;
|
||||||
|
|
||||||
// Update YjsDocument
|
// Debounce Yjs write (300ms) — only write after user pauses typing
|
||||||
const yjsDoc = yjsDocRef?.current;
|
if (yjsDebounceRef.current) {
|
||||||
if (yjsDoc) {
|
clearTimeout(yjsDebounceRef.current);
|
||||||
yjsDoc.updateElement(selectedElement.id, updates);
|
|
||||||
}
|
}
|
||||||
|
const elId = selectedElement.id;
|
||||||
|
yjsDebounceRef.current = setTimeout(() => {
|
||||||
|
const yjsDoc = yjsDocRef?.current;
|
||||||
|
if (yjsDoc) {
|
||||||
|
yjsDoc.updateElement(elId, updates);
|
||||||
|
}
|
||||||
|
yjsDebounceRef.current = null;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
// Update fabric object directly
|
// Update fabric object directly (immediate, no debounce)
|
||||||
const fabricObj = fabricObject;
|
const fabricObj = fabricObject;
|
||||||
const fabricCanvas = fabricCanvasRef.current;
|
const fabricCanvas = fabricCanvasRef.current;
|
||||||
if (fabricObj && fabricCanvas) {
|
if (fabricObj && fabricCanvas) {
|
||||||
@@ -251,7 +239,7 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef
|
|||||||
fabricCanvas.renderAll();
|
fabricCanvas.renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local state
|
// Update local state (immediate, no debounce)
|
||||||
setSelectedElement((prev) => {
|
setSelectedElement((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
return {
|
return {
|
||||||
@@ -264,7 +252,7 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef
|
|||||||
[selectedElement, fabricObject, yjsDocRef]
|
[selectedElement, fabricObject, yjsDocRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Field change handlers ─────────────────────────────────────────────────
|
// ── Field change handlers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleNumberChange = (field: 'x' | 'y' | 'width' | 'height', value: string) => {
|
const handleNumberChange = (field: 'x' | 'y' | 'width' | 'height', value: string) => {
|
||||||
const n = clampNumber(value, -100000, 100000);
|
const n = clampNumber(value, -100000, 100000);
|
||||||
@@ -297,7 +285,7 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ canvasRef, yjsDocRef
|
|||||||
updateFabricAndYjs({ layerId: value });
|
updateFabricAndYjs({ layerId: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────────────────
|
// ── Render ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (!selectedElement) {
|
if (!selectedElement) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user