fix multi-profile
This commit is contained in:
@@ -14,6 +14,7 @@ const AngleMeasureOverlay = lazy(() => import('./AngleMeasureOverlay'));
|
||||
const ThresholdHistogram = lazy(() => import('./ThresholdHistogram'));
|
||||
const RadialProfileOverlay = lazy(() => import('./RadialProfileOverlay'));
|
||||
const StraightenPathOverlay = lazy(() => import('./StraightenPathOverlay'));
|
||||
const MultiProfileOverlay = lazy(() => import('./MultiProfileOverlay'));
|
||||
|
||||
import TextNoteNode from './TextNoteNode';
|
||||
|
||||
@@ -1198,6 +1199,7 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
|
||||
|| data.overlay.kind === 'threshold_histogram'
|
||||
|| data.overlay.kind === 'radial_profile'
|
||||
|| data.overlay.kind === 'straighten_path'
|
||||
|| data.overlay.kind === 'multi_profile'
|
||||
);
|
||||
const hidePreviewForInteractiveMask = data.overlay?.kind === 'mask_paint' || data.overlay?.kind === 'markup';
|
||||
const overlayTitle = data.overlay?.section_title
|
||||
@@ -1217,6 +1219,8 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
|
||||
? 'Radial Profile'
|
||||
: data.overlay?.kind === 'straighten_path'
|
||||
? 'Path'
|
||||
: data.overlay?.kind === 'multi_profile'
|
||||
? 'Preview'
|
||||
: 'Cross Section');
|
||||
const headerMeta = (() => {
|
||||
if (data.className === 'Folder') {
|
||||
@@ -1572,6 +1576,15 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
|
||||
nodeId={id}
|
||||
onWidgetChange={ctx!.onWidgetChange}
|
||||
/>
|
||||
) : data.overlay!.kind === 'multi_profile' ? (
|
||||
<MultiProfileOverlay
|
||||
image={data.overlay!.image ?? ''}
|
||||
row={(data.overlay!.row ?? 0) as number}
|
||||
direction={(data.overlay!.direction ?? 'horizontal') as 'horizontal' | 'vertical'}
|
||||
maxIndex={(data.overlay!.max_index ?? 0) as number}
|
||||
nodeId={id}
|
||||
onWidgetChange={ctx!.onWidgetChange}
|
||||
/>
|
||||
) : data.overlay!.kind === 'angle_measure' ? (
|
||||
<AngleMeasureOverlay
|
||||
image={data.overlay!.image ?? ''}
|
||||
|
||||
89
frontend/src/MultiProfileOverlay.tsx
Normal file
89
frontend/src/MultiProfileOverlay.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { clamp, pointerToFraction } from './overlayUtils';
|
||||
|
||||
export const CAPTURE_SELECTOR = '.multiprofile-overlay';
|
||||
|
||||
interface MultiProfileOverlayProps {
|
||||
image: string;
|
||||
row: number;
|
||||
direction: 'horizontal' | 'vertical';
|
||||
maxIndex: number;
|
||||
nodeId: string;
|
||||
onWidgetChange: (nodeId: string, name: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
export default function MultiProfileOverlay({
|
||||
image, row, direction, maxIndex,
|
||||
nodeId, onWidgetChange,
|
||||
}: MultiProfileOverlayProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [draftRow, setDraftRow] = useState<number | null>(null);
|
||||
const draggingRef = useRef(false);
|
||||
const pendingCommitRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingCommitRef.current !== null && row === pendingCommitRef.current) {
|
||||
pendingCommitRef.current = null;
|
||||
setDraftRow(null);
|
||||
}
|
||||
}, [row]);
|
||||
|
||||
const displayRow = draftRow ?? row;
|
||||
|
||||
const fractionFromEvent = useCallback((e: React.PointerEvent<Element>): number => {
|
||||
if (!containerRef.current) return 0;
|
||||
const { fx, fy } = pointerToFraction(e, containerRef.current);
|
||||
return direction === 'horizontal' ? fy : fx;
|
||||
}, [direction]);
|
||||
|
||||
const fractionToIndex = useCallback((frac: number): number => {
|
||||
return clamp(Math.round(frac * maxIndex), 0, maxIndex);
|
||||
}, [maxIndex]);
|
||||
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
draggingRef.current = true;
|
||||
setDraftRow(fractionToIndex(fractionFromEvent(e)));
|
||||
}, [fractionFromEvent, fractionToIndex]);
|
||||
|
||||
const onPointerMove = useCallback((e: React.PointerEvent<Element>) => {
|
||||
if (!draggingRef.current) return;
|
||||
setDraftRow(fractionToIndex(fractionFromEvent(e)));
|
||||
}, [fractionFromEvent, fractionToIndex]);
|
||||
|
||||
const onPointerUp = useCallback(() => {
|
||||
if (draggingRef.current && draftRow !== null) {
|
||||
pendingCommitRef.current = draftRow;
|
||||
onWidgetChange(nodeId, 'row', draftRow);
|
||||
}
|
||||
draggingRef.current = false;
|
||||
}, [draftRow, nodeId, onWidgetChange]);
|
||||
|
||||
const fracPos = maxIndex > 0 ? displayRow / maxIndex : 0;
|
||||
const linePct = clamp(fracPos * 100, 0, 100);
|
||||
|
||||
const lineStyle: React.CSSProperties = direction === 'horizontal'
|
||||
? { left: 0, right: 0, top: `${linePct}%`, height: 0 }
|
||||
: { top: 0, bottom: 0, left: `${linePct}%`, width: 0 };
|
||||
|
||||
const cursorClass = direction === 'horizontal' ? 'cursor-row' : 'cursor-col';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`nodrag nowheel multiprofile-overlay ${cursorClass}`}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onLostPointerCapture={onPointerUp}
|
||||
>
|
||||
<img src={image} alt="A blended with B" draggable={false} className="multiprofile-image" />
|
||||
<div className={`multiprofile-line multiprofile-line-${direction}`} style={lineStyle} />
|
||||
<div className="multiprofile-readout">
|
||||
{direction === 'horizontal' ? 'row' : 'col'} {displayRow}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1736,6 +1736,43 @@ html, body, #root {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
/* ── Multi Profile overlay ────────────────────────────────────────── */
|
||||
.multiprofile-overlay {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.multiprofile-overlay.cursor-row { cursor: row-resize; }
|
||||
.multiprofile-overlay.cursor-col { cursor: col-resize; }
|
||||
.multiprofile-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.multiprofile-line {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.multiprofile-line-horizontal {
|
||||
border-top: 1.5px solid var(--marker);
|
||||
box-shadow: 0 0 4px var(--marker-shadow);
|
||||
}
|
||||
.multiprofile-line-vertical {
|
||||
border-left: 1.5px solid var(--marker);
|
||||
box-shadow: 0 0 4px var(--marker-shadow);
|
||||
}
|
||||
.multiprofile-readout {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
font-size: 10px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.angle-overlay {
|
||||
--angle-line-color: #ff9800;
|
||||
--angle-arc-color: rgb(255, 166, 77);
|
||||
@@ -1992,7 +2029,8 @@ html, body, #root {
|
||||
.is-panning .mask-paint-overlay,
|
||||
.is-panning .markup-overlay,
|
||||
.is-panning .radial-overlay,
|
||||
.is-panning .straighten-overlay {
|
||||
.is-panning .straighten-overlay,
|
||||
.is-panning .multiprofile-overlay {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ export interface OverlayData {
|
||||
thickness?: number;
|
||||
xres?: number;
|
||||
yres?: number;
|
||||
row?: number;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
max_index?: number;
|
||||
section_title?: string;
|
||||
line?: number[];
|
||||
shape?: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ export const OVERLAY_CAPTURE_SELECTORS = [
|
||||
'.angle-overlay', // AngleMeasureOverlay
|
||||
'.radial-overlay', // RadialProfileOverlay
|
||||
'.straighten-overlay', // StraightenPathOverlay
|
||||
'.multiprofile-overlay', // MultiProfileOverlay
|
||||
];
|
||||
|
||||
function encodeBase64(bytes: Uint8Array) {
|
||||
|
||||
Reference in New Issue
Block a user