3D model overlay UX improvements: button centering, WYSIWYG rotation, drop sizing

- Edit 3D button now renders at node center (AABB center) with translate-x/y-1/2,
  eliminating rotation misalignment
- Model3DOverlay card applies CSS rotate matching node.rotation (transform-origin 0 0)
  for WYSIWYG preview when the Konva node is rotated
- Dropped 3D models now centered at drop point and capped at 25% of canvas
  dimensions to prevent oversized placements; nativeWidth/nativeHeight preserved
  in params for correct re-editing
This commit is contained in:
Tanish Manku committed 2026-03-13 14:29:34 -07:00
1 parent 8bb8c186a3
commit e5f6d87f39
2 files changed
+41 -22

No files matched your search

@@ -71,10 +71,12 @@ const Edit3DButton = memo(function Edit3DButton({
}: Edit3DButtonProps) {
const [, forceUpdate] = useState(0);
const bump = useCallback(() => forceUpdate((t) => t + 1), []);
const [interacting, setInteracting] = useState(false);
// Attach Konva event listeners so the button repositions on every stage
// pan/zoom and on every transformer handle move (before onTransformEnd fires).
// Re-runs when nodeId changes so fresh transformers are picked up.
// Attach Konva event listeners:
// - Stage pan/zoom → bump() to reposition
// - Transformer start/end → hide/show button to avoid jitter
// - Node drag start/end → hide/show button
useEffect(() => {
const stage = stageRef.current;
if (!stage?.on) return;
@@ -84,35 +86,45 @@ const Edit3DButton = memo(function Edit3DButton({
`dragmove${ns} xChange${ns} yChange${ns} scaleXChange${ns} scaleYChange${ns}`,
bump,
);
const transformers = stage.find("Transformer");
transformers.forEach((tr) => tr.on(`transform${ns}`, bump));
transformers.forEach((tr) => {
tr.on(`transformstart${ns}`, () => setInteracting(true));
tr.on(`transformend${ns}`, () => setInteracting(false));
});
const konvaNode = stage.findOne("#" + nodeId);
if (konvaNode) {
konvaNode.on(`dragstart${ns}`, () => setInteracting(true));
konvaNode.on(`dragend${ns}`, () => setInteracting(false));
}
return () => {
stage.off(".edit3dbtn");
transformers.forEach((tr) => tr.off(".edit3dbtn"));
stage.off(ns);
transformers.forEach((tr) => tr.off(ns));
konvaNode?.off(ns);
};
}, [stageRef, bump, nodeId]);
if (interacting) return null;
const stage = stageRef.current;
if (!stage?.container) return null;
const stageRect = stage.container().getBoundingClientRect();
const sx = stage.scaleX();
const sy = stage.scaleY();
// Read live Konva node geometry so the button tracks transformer handles
// in real-time (the store value is stale until onTransformEnd commits it).
const konvaNode = stage.findOne("#" + nodeId) as Konva.Shape | undefined;
if (!konvaNode) return null;
const screenLeft = stageRect.left + stage.x() + konvaNode.x() * sx;
const screenTop = stageRect.top + stage.y() + konvaNode.y() * sy;
const screenW = konvaNode.width() * konvaNode.scaleX() * sx;
// getClientRect() returns the AABB in stage-container coords after all
// transforms (rotation, scale, stage pan/zoom) — no manual math needed.
const clientRect = konvaNode.getClientRect();
const stageContainerRect = stage.container().getBoundingClientRect();
const btnLeft = stageContainerRect.left + clientRect.x + clientRect.width / 2;
const btnTop = stageContainerRect.top + clientRect.y + clientRect.height / 2;
return (
<button
className="pointer-events-auto fixed z-40 -translate-x-1/2 rounded-full bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-lg hover:bg-blue-500"
style={{ left: screenLeft + screenW / 2, top: screenTop - 36 }}
className="pointer-events-auto fixed z-40 -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-lg hover:bg-blue-500"
style={{ left: btnLeft, top: btnTop }}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onEdit(nodeId)}
>
@@ -382,14 +394,19 @@ const PageDraw = () => {
);
const img = new globalThis.Image();
img.onload = () => {
const canvasDims = getAspectRatioDimensions();
const maxDim = Math.min(canvasDims.width, canvasDims.height) * 0.25;
const aspect = img.width / img.height;
const displayW = Math.min(img.width, maxDim * Math.max(1, aspect));
const displayH = displayW / aspect;
createImageFrom3DModel(
stagePoint.x,
stagePoint.y,
stagePoint.x - displayW / 2,
stagePoint.y - displayH / 2,
dataUrl,
modelUrl,
DEFAULT_MODEL3D_PARAMS,
img.width,
img.height,
{ ...DEFAULT_MODEL3D_PARAMS, nativeWidth: img.width, nativeHeight: img.height },
displayW,
displayH,
);
toast.success(`Added "${item.label}" to canvas`, { id: toastId });
};
@@ -296,6 +296,8 @@ export function Model3DOverlay({
top: screenRect.top,
width: screenRect.width,
height: screenRect.height,
transform: `rotate(${node.rotation ?? 0}deg)`,
transformOrigin: "0 0",
}}
onPointerDown={(e) => e.stopPropagation()}
>