Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 51 additions & 35 deletions site/src/components/ShapeBuilder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { Wrapper, CanvasContainer, OutputBox, StyledSVG, CopyButton } from "./sh
import { Button, Typography, Box, CopyIcon, Select, MenuItem, Slider, FormControl } from "@sistent/sistent";
import { SVG, extend as SVGextend } from "@svgdotjs/svg.js";
import draw from "@svgdotjs/svg.draw.js";
import CoordinatesModal from "./CoordinatesModal";

SVGextend(SVG.Polygon, draw);

const SCALE_PRESETS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3];
const MIN_SCALE = 0.1;
const MAX_SCALE = 3;

const ShapeBuilder = () => {
const ShapeBuilder = ({ theme }) => {
const boardRef = useRef(null);
const polyRef = useRef(null);
const keyHandlersRef = useRef({});
Expand All @@ -22,9 +23,11 @@ const ShapeBuilder = () => {
const [scale, setScale] = useState(1);
const [currentPreset, setCurrentPreset] = useState(1);

// ── Modal state ─────────────────────────────────────────────────────────────
const [modalOpen, setModalOpen] = useState(false);

const handleCopyToClipboard = async () => {
if (!result.trim()) return;

try {
await navigator.clipboard.writeText(result);
setShowCopied(true);
Expand All @@ -44,17 +47,17 @@ const ShapeBuilder = () => {
const showCytoArray = () => {
const poly = polyRef.current;
if (!poly) return;

try {
const points = getPlottedPoints(poly);
if (!points) throw new Error("Invalid or empty polygon points");

const normalized = points
.map(([x, y]) => [(x - 260) / 260, (y - 260) / 260])
.flat()
.join(" ");
setResult(normalized);
setError(null);
// ── Open the modal whenever coordinates are ready ──
setModalOpen(true);
} catch (err) {
setError("Failed to extract and normalize polygon points.");
console.error("showCytoArray error:", err);
Expand All @@ -64,38 +67,30 @@ const ShapeBuilder = () => {
const applyScale = (newScale) => {
const poly = polyRef.current;
if (!poly) return;

const points = getPlottedPoints(poly);
if (!points || points.length === 0) return;

if (!basePointsRef.current) {
basePointsRef.current = points;
}

const basePoints = basePointsRef.current;

const xs = basePoints.map(p => p[0]);
const ys = basePoints.map(p => p[1]);
const centerX = (Math.max(...xs) + Math.min(...xs)) / 2;
const centerY = (Math.max(...ys) + Math.min(...ys)) / 2;

const scaledPoints = basePoints.map(([x, y]) => {
const dx = x - centerX;
const dy = y - centerY;
return [centerX + dx * newScale, centerY + dy * newScale];
});

poly.plot(scaledPoints);
showCytoArray();
};

const handleScaleChange = (newScale) => {
const clampedScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));
setScale(clampedScale);

const matchingPreset = SCALE_PRESETS.find(p => Math.abs(p - clampedScale) < 0.01);
setCurrentPreset(matchingPreset || clampedScale);

applyScale(clampedScale);
};

Expand All @@ -113,17 +108,14 @@ const ShapeBuilder = () => {
const handleKeyDown = (e) => {
const poly = polyRef.current;
if (!poly) return;

if (e.ctrlKey) {
poly.draw("param", "snapToGrid", 0.001);
}

if (e.key === "Enter" || e.key === "Escape") {
poly.draw("done");
poly.fill("#00B39F");
showCytoArray();
}

if (e.ctrlKey && e.key.toLowerCase() === "z") {
const points = getPlottedPoints(poly);
if (!points) return;
Expand Down Expand Up @@ -155,20 +147,17 @@ const ShapeBuilder = () => {
setError("Canvas reference not found");
return;
}

try {
const draw = SVG()
const drawInstance = SVG()
.addTo(boardRef.current)
.size("100%", "100%")
.polygon()
.draw()
.attr({ stroke: "#00B39F", "stroke-width": 1, fill: "none" });

draw.draw("param", "snapToGrid", 16);
draw.on("drawstart", attachKeyListeners);
draw.on("drawdone", detachKeyListeners);

polyRef.current = draw;
drawInstance.draw("param", "snapToGrid", 16);
drawInstance.on("drawstart", attachKeyListeners);
drawInstance.on("drawdone", detachKeyListeners);
polyRef.current = drawInstance;
setError(null);
} catch (err) {
setError(`Failed to initialize drawing: ${err.message}`);
Expand All @@ -177,23 +166,23 @@ const ShapeBuilder = () => {

const clearShape = () => {
const poly = polyRef.current;
if (!poly) return;

poly.draw("cancel");
poly.remove();
detachKeyListeners();
polyRef.current = null;
basePointsRef.current = null;
if (poly) {
poly.draw("cancel");
poly.remove();
detachKeyListeners();
polyRef.current = null;
basePointsRef.current = null;
}
setResult("");
setScale(1);
setCurrentPreset(1);
setModalOpen(false);
initializeDrawing();
};

const closeShape = () => {
const poly = polyRef.current;
if (!poly) return;

poly.draw("done");
poly.fill("#00B39F");
const points = getPlottedPoints(poly);
Expand All @@ -217,6 +206,16 @@ const ShapeBuilder = () => {

return (
<Wrapper>
{/* ── Coordinates Modal ─────────────────────────────────────────────── */}
{modalOpen && result && (
<CoordinatesModal
coordinates={result}
theme={theme}
onClose={() => setModalOpen(false)}
onClear={clearShape}
/>
)}

<CanvasContainer>
<StyledSVG
ref={boardRef}
Expand Down Expand Up @@ -250,6 +249,24 @@ const ShapeBuilder = () => {
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", gap: 2, mt: 3, mb: 3, flexWrap: "wrap" }}>
<Button variant="contained" onClick={clearShape}>Clear</Button>
<Button variant="contained" onClick={closeShape}>Close Shape</Button>
{/* Re-open modal button — only shown when coordinates exist */}
{result && (
<Button
variant="outlined"
onClick={() => setModalOpen(true)}
title="View polygon coordinates"
sx={{
color: "#00B39F",
borderColor: "#00B39F",
"&:hover": {
borderColor: "#00B39F",
backgroundColor: "rgba(0, 179, 159, 0.08)",
},
}}
>
View Coordinates
</Button>
)}

<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, ml: 2 }}>
<FormControl size="small" sx={{ minWidth: 80 }}>
Expand All @@ -261,9 +278,7 @@ const ShapeBuilder = () => {
aria-label="Scale preset"
sx={{
color: "#fff",
"& .MuiSelect-icon": {
color: "#fff"
}
"& .MuiSelect-icon": { color: "#fff" }
}}
>
{SCALE_PRESETS.map((preset) => (
Expand Down Expand Up @@ -295,12 +310,13 @@ const ShapeBuilder = () => {
</Box>
</Box>

{/* ── Fallback output box (kept for accessibility / non-JS contexts) ── */}
<OutputBox>
<Typography variant="subtitle1" component="h6">
Polygon Coordinates (SVG format):
</Typography>
<div style={{ position: "relative" }}>
<textarea readOnly value={result} />
<textarea readOnly value={result} aria-label="Polygon coordinates output" />
{result.trim() && (
<CopyButton
onClick={handleCopyToClipboard}
Expand Down
21 changes: 15 additions & 6 deletions site/src/components/ShapeBuilder/shapeBuilder.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ export const OutputBox = styled.div`
textarea {
width: 100%;
height: 80px;
padding: 1rem;
padding: 1rem 2.5rem 1rem 1rem;
border: 1px solid ${({ theme }) => theme.border || "#24292E"};
border-radius: 0.5rem;
background-color: ${({ theme }) => theme.body || "#181B1F"};
color: ${({ theme }) => theme.text || "#fff"};
resize: none;
font-family: monospace;
font-size: 0.95rem;
box-sizing: border-box;
}

.error {
Expand All @@ -121,16 +122,24 @@ export const OutputBox = styled.div`

export const CopyButton = styled.button`
position: absolute;
top: 0;
right: -25px;
background: none;
border: none;
top: 8px;
right: 8px;
background: ${({ theme }) => theme.body || "#181B1F"};
border: 1px solid ${({ theme }) => theme.border || "#24292E"};
border-radius: 4px;
cursor: pointer;
padding: 4px;
padding: 4px 6px;
display: flex;
align-items: center;
font-size: 12px;
color: ${({ theme }) => theme.text};
opacity: 0.85;
transition: opacity 0.15s;
z-index: 1;

&:hover {
opacity: 1;
}

svg {
color: ${({ theme }) => theme.text};
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const IndexPage = () => {
</Box>
</section>

<ShapeBuilder />
<ShapeBuilder theme={activeTheme} />
</Main>
<Footer />
</ThemeProvider>
Expand Down
Loading