diff --git a/PanTS-Demo/src/components/segmentation/FillBetweenSlicesFlyout.tsx b/PanTS-Demo/src/components/segmentation/FillBetweenSlicesFlyout.tsx
index 95172ce7..372eceaf 100644
--- a/PanTS-Demo/src/components/segmentation/FillBetweenSlicesFlyout.tsx
+++ b/PanTS-Demo/src/components/segmentation/FillBetweenSlicesFlyout.tsx
@@ -27,7 +27,13 @@ export default function FillBetweenSlicesFlyout({ segmentIndex, maskFilter, onLo
const picker = useSliceAnchorPicker({
segmentIndex,
- lastRequiresSegment: false,
+ // Unlike Copy-across-slices (whose destination legitimately doesn't
+ // need existing content), Fill needs a real segment on BOTH ends to
+ // interpolate between — a blank second slice has nothing to fill
+ // toward. Was `false` (copied from CopyAcrossSlicesFlyout without
+ // updating for this), which let step 2 accept any slice with no
+ // segment check at all.
+ lastRequiresSegment: true,
onError: (d) => { setPickError(d); onLog?.(d); },
});
const { phase, step, first, last } = picker;
diff --git a/PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css b/PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css
index 2db02c56..548404f5 100644
--- a/PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css
+++ b/PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css
@@ -311,7 +311,7 @@
.seg-effect__number:focus {
outline: none;
- border-color: #E76F51;
+ border-color: #0b17ff;
background: rgba(104, 172, 229, 0.08);
}
@@ -387,7 +387,7 @@
.seg-effect__icon-btn--primary {
border-color: rgba(15, 23, 42, 0.4);
- color: #E76F51;
+ color: #0b17ff;
}
.seg-effect__icon-btn--primary:hover:not(:disabled) {
diff --git a/PanTS-Demo/src/components/segmentation/SegmentsPopup.css b/PanTS-Demo/src/components/segmentation/SegmentsPopup.css
index 9b504606..fc6a999b 100644
--- a/PanTS-Demo/src/components/segmentation/SegmentsPopup.css
+++ b/PanTS-Demo/src/components/segmentation/SegmentsPopup.css
@@ -278,7 +278,7 @@
}
.segpop__color-popover-swatch.is-active {
- outline: 2px solid #E76F51;
+ outline: 2px solid #7cb7da;
outline-offset: 1px;
}
@@ -303,6 +303,53 @@
appearance: none;
}
+/* Explicit commit footer for the color popover — live preview + hex value
+ + a clear "Done" close button, so picking a color visibly registers
+ before the popover disappears instead of a swatch click silently
+ closing it with no confirmation of what was chosen. Doesn't itself save
+ the class (that's still the row's own Save/ApplyButton); this is purely
+ about the popover's own selection state being unambiguous. */
+.segpop__color-popover-footer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 2px;
+ padding-top: 8px;
+ border-top: 1px solid rgba(255, 255, 255, 0.09);
+}
+
+.segpop__color-popover-preview {
+ width: 22px;
+ height: 22px;
+ border-radius: 6px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ flex-shrink: 0;
+}
+
+.segpop__color-popover-hex {
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: 0.02em;
+ color: rgba(255, 255, 255, 0.7);
+ flex: 1;
+}
+
+.segpop__color-popover-done {
+ appearance: none;
+ border: none;
+ border-radius: 6px;
+ padding: 5px 12px;
+ font-size: 11px;
+ font-weight: 600;
+ background: #7cb7da;
+ color: #16181d;
+ cursor: pointer;
+}
+
+.segpop__color-popover-done:hover {
+ background: #7cb7da;
+}
+
/* Static color chip shown in the normal (non-editing) row — color is only
changed together with the name via the pen-icon editor, so this isn't an
and doesn't open a picker on click. */
@@ -327,7 +374,7 @@
width: 18px;
height: 18px;
border-radius: 50%;
- color: #E76F51;
+ color: #7cb7da;
background: rgba(104, 172, 229, 0.16);
}
@@ -339,8 +386,8 @@
}
.segpop__target-badge--btn:hover {
- background: rgba(232, 93, 93, 0.22);
- color: #E85D5D;
+ background: rgb(17, 0, 75);
+ color: #7cb7da;
transform: scale(1.08);
}
@@ -380,7 +427,7 @@
.segpop__edit-btn:hover,
.segpop__edit-btn.is-active {
- color: #E76F51;
+ color: #7cb7da;
}
/* Floating add/edit form (see FormFlyout in SegmentsPopup.tsx). Portaled to
@@ -515,7 +562,7 @@
}
.segpop__delete:hover:not(:disabled) {
- color: #E76F51;
+ color: #7cb7da;
}
.segpop__err {
@@ -653,7 +700,7 @@
width: 6px;
height: 6px;
border-radius: 50%;
- background: #E76F51;
+ background: #7cb7da;
}
.segpop__tab.is-active .segpop__tab-dot {
diff --git a/PanTS-Demo/src/components/segmentation/SegmentsPopup.tsx b/PanTS-Demo/src/components/segmentation/SegmentsPopup.tsx
index f7828cf4..732dd888 100644
--- a/PanTS-Demo/src/components/segmentation/SegmentsPopup.tsx
+++ b/PanTS-Demo/src/components/segmentation/SegmentsPopup.tsx
@@ -105,6 +105,7 @@ interface ColorPickerPopoverProps {
function ColorPickerPopover({ value, onChange, onClose, anchorRef }: ColorPickerPopoverProps) {
const [closing, setClosing] = useState(false);
const popRef = useRef(null);
+ const colorInputRef = useRef(null);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const requestClose = () => {
@@ -146,6 +147,16 @@ function ColorPickerPopover({ value, onChange, onClose, anchorRef }: ColorPicker
const onDown = (e: MouseEvent) => {
if (popRef.current?.contains(e.target as Node)) return;
if (anchorRef.current?.contains(e.target as Node)) return;
+ // The native OS color picker (behind the "Custom…" ) can fire a synthetic mousedown on `document`
+ // itself — outside both the input and this whole component tree —
+ // when the user drags/picks within the OS dialog, in some
+ // browsers. Without this guard that got misread as "clicked
+ // outside the popover" and closed it mid-pick, which is exactly
+ // why changing the color via the custom picker felt broken —
+ // every drag/selection risked closing before it registered. Skip
+ // closing while the color input itself still has focus.
+ if (document.activeElement === colorInputRef.current) return;
requestClose();
};
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") requestClose(); };
@@ -163,6 +174,7 @@ function ColorPickerPopover({ value, onChange, onClose, anchorRef }: ColorPicker
return createPortal(
e.stopPropagation()}
@@ -175,14 +187,34 @@ function ColorPickerPopover({ value, onChange, onClose, anchorRef }: ColorPicker
className={`segpop__color-popover-swatch ${value.toLowerCase() === hex.toLowerCase() ? "is-active" : ""}`}
style={{ background: hex }}
aria-label={hex}
+ // Stages the color (updates the draft the caller holds) but does
+ // NOT close the popover — picking a swatch used to auto-close
+ // immediately, which looked like "nothing happened" since the
+ // actual save is a separate action on the row below. Leaving it
+ // open lets the person see the live preview/hex below update
+ // and confirm with an explicit "Done" instead.
onClick={() => onChange(hex)}
/>
))}
+ {/* Explicit commit step for the popover itself — a live preview swatch
+ plus the hex value, so it's visually obvious a selection has been
+ made and staged, then "Done" closes the popover. This does NOT save
+ the class — that's still the row's own Save/ApplyButton — it just
+ makes clear the color choice registered before the popover goes
+ away, instead of a swatch click silently vanishing the popover with
+ no confirmation of what got picked. */}
+
+
+ {value.toUpperCase()}
+
+
,
document.body
);
@@ -268,6 +300,16 @@ function FormFlyout({ anchorEl, onClose, children }: FormFlyoutProps) {
const onDown = (e: MouseEvent) => {
if (panelRef.current?.contains(e.target as Node)) return;
if (anchorEl?.contains(e.target as Node)) return;
+ // The color swatch popover portals to document.body on its OWN,
+ // separate from this FormFlyout's portal — so a click on a preset
+ // swatch or the native color input isn't a descendant of either
+ // panelRef or anchorEl, and without this check got misread as
+ // "clicked outside the name/color editor," closing the whole
+ // editor instead of just the small color popover. The color
+ // popover already closes itself independently on its own outside
+ // click; this just stops THIS flyout from also reacting to a
+ // click that was actually still inside it, conceptually.
+ if ((e.target as Element | null)?.closest?.("[data-color-popover-portal]")) return;
requestClose();
};
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") requestClose(); };
diff --git a/PanTS-Demo/src/components/viewer/AnnotationToolbar.css b/PanTS-Demo/src/components/viewer/AnnotationToolbar.css
index 52f88603..de48b81c 100644
--- a/PanTS-Demo/src/components/viewer/AnnotationToolbar.css
+++ b/PanTS-Demo/src/components/viewer/AnnotationToolbar.css
@@ -152,7 +152,7 @@
.atb-flyout__range:active::-webkit-slider-thumb {
background: #0F172A;
- border-color: #E76F51;
+ border-color: #68ACE5;
}
.atb-flyout__range::-moz-range-track {
@@ -348,8 +348,8 @@
background: rgba(34, 34, 42, 0.62);
backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(20px) saturate(150%);
- border-left: 2px solid #E76F51;
- border-top: 2px solid #E76F51;
+ border-left: 2px solid #68ACE5;
+ border-top: 2px solid #68ACE5;
border-radius: 4px 0 0 0;
transform: translateX(-50%) rotate(45deg);
transition: left 0.2s ease;
@@ -920,7 +920,7 @@
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
- color: var(--accent, #E76F51);
+ color: var(--jhu-blue-accent, #68ACE5);
margin: 0 0 8px;
}
@@ -979,7 +979,7 @@
padding: 10px 0;
border-radius: 999px;
background: var(--jhu-blue, #0F172A);
- border: 1px solid var(--accent, #E76F51);
+ border: 1px solid var(--jhu-blue-accent, #68ACE5);
color: #ffffff;
font-size: 12.5px;
font-weight: 800;
diff --git a/PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx b/PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx
index ee68a132..9af523e4 100644
--- a/PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx
+++ b/PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx
@@ -130,6 +130,15 @@ interface AnnotationToolbarProps {
// and the guided-overlay tools (close once their full-screen walkthrough
// takes over).
renderFlyout: (tool: Exclude, onApplied: () => void, onCloseSettings: () => void, onGuidedControlsChange: (controls: GuidedFlowControls | null) => void) => React.ReactNode;
+ /** Fired whenever a guided slice-anchor pick flow (Copy/Fill across
+ * slices) becomes active or finishes — true for the flow's entire
+ * lifecycle (both click steps, the ready-to-apply confirm, and while
+ * committing), not just the literal picking sub-phase, since the
+ * crosshair shouldn't be live for any of it: clicking a pane during a
+ * guided pick is meant to choose a slice/anchor, not move the
+ * crosshair. The caller (VisualizationPage) uses this to suppress
+ * Crosshairs for the duration. */
+ onGuidedPickingChange?: (active: boolean) => void;
popupRef?: React.RefObject;
popupDragRef?: React.RefObject;
@@ -146,20 +155,20 @@ interface AnnotationToolbarProps {
}
const TOOL_DEFS: Array<{ id: Exclude; label: string; Icon: typeof IconBrush; description: string }> = [
- { id: "paint", label: "Brush", Icon: IconBrush, description: "Paint with a round brush." },
- { id: "erase", label: "Erase", Icon: IconEraser, description: "Erase with a round brush." },
- { id: "scissors", label: "Scissors", Icon: IconScissors, description: "Cut through the entire class from the current viewpoint." },
- { id: "levelTracing", label: "Level Tracing", Icon: IconRipple, description: "Trace the boundary of similar intensity around the cursor." },
- { id: "margin", label: "Margin", Icon: IconArrowsDiagonal, description: "Grow or shrink the selected class by a specified margin size." },
- { id: "smoothing", label: "Smoothing", Icon: IconWaveSine, description: "Make class boundaries smoother." },
+ { id: "paint", label: "Brush", Icon: IconBrush, description: "Paint freehand with a round brush." },
+ { id: "erase", label: "Erase", Icon: IconEraser, description: "Erase parts of a shape manually." },
+ { id: "scissors", label: "Scissors", Icon: IconScissors, description: "Lasso tool using anchor points." },
+ { id: "levelTracing", label: "Level Tracing", Icon: IconRipple, description: "Traces the boundary of similar intensity around cursor." },
+ { id: "margin", label: "Margin", Icon: IconArrowsDiagonal, description: "Grow or shrink by a specified margin size." },
+ { id: "smoothing", label: "Smoothing", Icon: IconWaveSine, description: "Smooth class boundaries." },
{ id: "islands", label: "Islands", Icon: IconDroplet, description: "Edit islands (connected components) in a class." },
{ id: "logicalOperators", label: "Logical operators", Icon: IconMathFunction, description: "Apply logical operators or combine classes." },
- { id: "growFromSeeds", label: "Grow from seeds", Icon: IconWand, description: "Grow a class from user-placed seed scribbles." },
+ { id: "growFromSeeds", label: "Grow from seeds", Icon: IconWand, description: "Grow a class from user-placed scribbles." },
{ id: "fillBetweenSlices", label: "Fill between slices", Icon: IconStack2, description: "Interpolate a class's shape between two annotated slices." },
- { id: "copyAcrossSlices", label: "Copy across slices", Icon: IconCopy, description: "Copy a class's shape from one slice across a range." },
+ { id: "copyAcrossSlices", label: "Copy across slices", Icon: IconCopy, description: "Copy a class's shape from first to last slice." },
{ id: "hollow", label: "Hollow", Icon: IconCircleDashed, description: "Make the class hollow by replacing it with a uniform-thickness shell." },
- { id: "pointSegment", label: "Click to segment", Icon: IconPointer, description: "Click a point to propose a segment there (grows a region from the click; box-restrict or refine after)." },
- { id: "boxSegment", label: "Box to segment", Icon: IconFrame, description: "Draw a box to propose a segment restricted to that region." },
+ { id: "pointSegment", label: "Click to segment", Icon: IconPointer, description: "Click an object to fill its shape" },
+ { id: "boxSegment", label: "Box to segment", Icon: IconFrame, description: "Draw a box to find a shape inside that area." },
];
const SCISSORS_OPERATIONS: { value: ScissorsOperation; label: string }[] = [
@@ -191,7 +200,7 @@ const GUIDED_HINT_COPY: Record = {
growSeeds:
"Continue moves on once you've placed your seed scribbles. Start over clears every seed and lets you begin again. Exit leaves Grow from Seeds without changing anything.",
sliceOps:
- "Continue (labeled Remove picked/Keep picked/etc. depending on the tool) applies your picks. Start over clears them and lets you pick again. Exit leaves the tool without changing anything.",
+ "Start over clears any choices made and lets you pick again. Exit leaves the tool without changing anything.",
};
// Ribbon height, matches --atb-ribbon-h in CSS. Exported so SegmentsPopup
@@ -393,7 +402,7 @@ function RenderingIndicator({ label: _label, visible }: { label?: string; visibl
width: 7,
height: 7,
borderRadius: "50%",
- background: "var(--accent, #E76F51)",
+ background: "var(--jhu-blue-accent, #68ACE5)",
animation: "seg-effect-render-pulse 0.9s ease-in-out infinite",
}}
/>
@@ -465,7 +474,7 @@ export default function AnnotationToolbar({
renderFlyout, scissorsPointCount, onScissorsCancel, maskingArea,
onMaskingAreaChange, hasAnySegments, scopeLocked, isRendering, isDeletingSegment,
targetKey, showOnlyTargetMask, onShowOnlyTargetMaskChange,
- popupRef, popupDragRef, popupMinRef, anchorRef,
+ popupRef, popupDragRef, popupMinRef, anchorRef, onGuidedPickingChange,
}: AnnotationToolbarProps) {
const [hoveredTool, setHoveredTool] = useState(null);
const [hoveredRect, setHoveredRect] = useState(null);
@@ -854,6 +863,9 @@ export default function AnnotationToolbar({
useEffect(() => {
guidedControlsRef.current = guidedControls;
}, [guidedControls]);
+ useEffect(() => {
+ onGuidedPickingChange?.(guidedControls != null);
+ }, [guidedControls, onGuidedPickingChange]);
// Small non-blocking hint shown right next to the cursor when Continue
// is clicked while `continueDisabled` — e.g. "Mark at least one point
@@ -1012,11 +1024,17 @@ export default function AnnotationToolbar({
aria-orientation="horizontal"
>
-
+
{TOOL_DEFS.map(({ id, label, Icon, description }) => {
// Only equip-and-use tools (paint/erase/scissors/level tracing)
// get a settings arrow; other tools open settings on icon click.
- const hasSettingsArrow = LIVE_COMMIT_TOOLS.includes(id);
+ // pointSegment/boxSegment are equip-and-use too (no separate
+ // settings step needed to click/drag) but have no configurable
+ // settings at all, so they're excluded from the arrow itself even
+ // though they stay in LIVE_COMMIT_TOOLS for the rest of its
+ // behavior (outside-click keeps them armed, etc).
+ const hasSettingsArrow =
+ LIVE_COMMIT_TOOLS.includes(id) && id !== "pointSegment" && id !== "boxSegment";
const settingsOpenHere = toolFlyout.open && activeTool === id;
return (
{
+ // Mirrors the main icon button's disabled-click handling above:
+ // the arrow previously called openToolSettings unconditionally,
+ // even when `enabled` was false, silently opening a tool's
+ // settings flyout with no class selected. Route it through the
+ // same "pick a class first" walkthrough popout instead.
+ if (!enabled) { setPickClassHintOpen(true); return; }
if (settingsOpenHere) toolFlyout.setOpen(false);
else openToolSettings(id);
}}
@@ -1086,7 +1110,7 @@ export default function AnnotationToolbar({
flexShrink: 0,
display: "flex",
alignItems: "center",
- gap: 8,
+ gap: 15,
marginLeft: 14,
paddingLeft: 14,
borderLeft: "1px solid rgba(255, 255, 255, 0.09)",
@@ -1123,7 +1147,7 @@ export default function AnnotationToolbar({
width: 7,
height: 7,
borderRadius: "50%",
- background: "var(--accent, #E76F51)",
+ background: "var(--jhu-blue-accent, #68ACE5)",
animation: "seg-effect-render-pulse 0.9s ease-in-out infinite",
}}
/>
@@ -1347,7 +1371,7 @@ export default function AnnotationToolbar({
left: pickClassHintRect.left - 3,
width: pickClassHintRect.width + 6,
height: pickClassHintRect.height + 3,
- border: "2px dashed var(--accent, #E76F51)",
+ border: "2px dashed var(--jhu-blue-accent, #68ACE5)",
borderRadius: 12,
pointerEvents: "none",
zIndex: 120,
@@ -1372,7 +1396,7 @@ export default function AnnotationToolbar({
}}
>
- Select an existing class or create a custom one, then start annotating.
+ Select an existing class or create a custom one to start annotating.