加个橡皮擦

This commit is contained in:
cloud
2026-07-17 17:40:56 +08:00
parent 84d67dc431
commit 1e19485845
3 changed files with 198 additions and 19 deletions
+49
View File
@@ -75,6 +75,45 @@
color: #fde68a;
}
.editToolGroup {
display: inline-flex;
align-items: center;
overflow: hidden;
border: 1px solid #343449;
border-radius: 8px;
background: #151520;
}
.toolButton {
min-height: 34px;
border: 0;
border-right: 1px solid #343449;
background: transparent;
color: #cbd5e1;
padding: 0 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.toolButton:last-child {
border-right: 0;
}
.toolButton:hover {
background: #24243a;
}
.toolButton.active {
background: rgba(251, 191, 36, 0.14);
color: #fde68a;
}
.toolButton.erase.active {
background: rgba(248, 113, 113, 0.14);
color: #fecaca;
}
.uploadButton input {
display: none;
}
@@ -607,6 +646,16 @@ select:focus {
background: rgba(251, 191, 36, 0.16);
}
.chartBrush.erase {
border-color: rgba(248, 113, 113, 0.95);
background: rgba(248, 113, 113, 0.08);
box-shadow: 0 0 0 1px rgba(16, 16, 26, 0.8), 0 0 18px rgba(248, 113, 113, 0.24);
}
.chartBrush.erase.dragging {
background: rgba(248, 113, 113, 0.16);
}
.chartTools {
display: flex;
align-items: center;
+148 -19
View File
@@ -49,7 +49,9 @@ const DESIGN_FIELD_HINTS = {
const CHUNK_SIZE = 4000;
const CHART_HEIGHT = 540;
const BRUSH_RADIUS = 16;
const BRUSH_RADIUS = 10;
const EDIT_TOOL_RESTORE = 'restore';
const EDIT_TOOL_ERASE = 'erase';
const DEFAULT_SCHEME_ID = 'scheme_one';
const SCHEME_TWO_ID = 'scheme_two';
const DEFAULT_SCHEMES = [
@@ -436,7 +438,9 @@ function PowerCurveChart({
showFiltered,
designPoints,
editMode,
editTool,
onRestorePoints,
onErasePoints,
}) {
const frameRef = useRef(null);
const chartRef = useRef(null);
@@ -587,14 +591,17 @@ function PowerCurveChart({
dragging: nextDragging,
});
};
const restoreAt = (event) => {
if (!chart.over || !onRestorePoints) return;
const applyToolAt = (event) => {
if (!chart.over) return;
const rect = chart.over.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const radiusSquared = BRUSH_RADIUS * BRUSH_RADIUS;
const restored = [];
for (const point of validFiltered) {
const targetPoints = editTool === EDIT_TOOL_ERASE
? validScatter
: validFiltered.filter((point) => point.reason !== 'manual_erased');
const hitPoints = [];
for (const point of targetPoints) {
const pointX = chart.valToPos(point.wind_speed, 'x');
const pointY = chart.valToPos(point.active_power, 'y');
if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) {
@@ -603,11 +610,16 @@ function PowerCurveChart({
const dx = pointX - x;
const dy = pointY - y;
if (dx * dx + dy * dy <= radiusSquared) {
restored.push(point);
hitPoints.push(point);
}
}
if (restored.length) {
onRestorePoints(restored);
if (!hitPoints.length) {
return;
}
if (editTool === EDIT_TOOL_ERASE) {
onErasePoints?.(hitPoints);
} else {
onRestorePoints?.(hitPoints);
}
};
const handlePointerDown = (event) => {
@@ -615,12 +627,12 @@ function PowerCurveChart({
event.preventDefault();
dragging = true;
updateBrush(event, true);
restoreAt(event);
applyToolAt(event);
};
const handlePointerMove = (event) => {
updateBrush(event);
if (dragging) {
restoreAt(event);
applyToolAt(event);
}
};
const handlePointerUp = (event) => {
@@ -647,7 +659,9 @@ function PowerCurveChart({
validDesign,
showFiltered,
editMode,
editTool,
onRestorePoints,
onErasePoints,
]);
useEffect(() => {
@@ -661,7 +675,12 @@ function PowerCurveChart({
}
return (
<div className={`chartFrame ${editMode ? 'chartFrameEditing' : ''}`} ref={frameRef}>
<div
className={`chartFrame ${editMode ? 'chartFrameEditing' : ''} ${
editTool === EDIT_TOOL_ERASE ? 'chartFrameErasing' : ''
}`}
ref={frameRef}
>
<div className="chartLegend">
<span><i className="legendDot scatter" />散点数据</span>
<span><i className="legendLine actual" />实际功率</span>
@@ -670,7 +689,9 @@ function PowerCurveChart({
<div className="uplotWrap" ref={chartRef} />
{editMode && brush.visible && (
<div
className={`chartBrush ${brush.dragging ? 'dragging' : ''}`}
className={`chartBrush ${editTool === EDIT_TOOL_ERASE ? 'erase' : ''} ${
brush.dragging ? 'dragging' : ''
}`}
style={{
width: BRUSH_RADIUS * 2,
height: BRUSH_RADIUS * 2,
@@ -693,7 +714,9 @@ export default function HomePage() {
const [selectedFan, setSelectedFan] = useState('');
const [showFiltered, setShowFiltered] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [editTool, setEditTool] = useState(EDIT_TOOL_RESTORE);
const [restoredPointIds, setRestoredPointIds] = useState({});
const [erasedPointIds, setErasedPointIds] = useState({});
const [readingDesign, setReadingDesign] = useState(false);
const [designRows, setDesignRows] = useState(() => createDesignRows());
const [designMeta, setDesignMeta] = useState(null);
@@ -710,9 +733,16 @@ export default function HomePage() {
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
const selectedScatter = selectedFan && result?.scatter_points
const selectedScatterRaw = selectedFan && result?.scatter_points
? result.scatter_points[selectedFan] || []
: [];
const selectedScatter = useMemo(
() => withPointIds(selectedScatterRaw, selectedFan).map((point) => ({
...point,
edit_origin: 'scatter',
})),
[selectedScatterRaw, selectedFan],
);
const selectedFilteredRaw = selectedFan && result?.filtered_points
? result.filtered_points[selectedFan] || []
: [];
@@ -724,17 +754,40 @@ export default function HomePage() {
() => new Set(restoredPointIds[selectedFan] || []),
[restoredPointIds, selectedFan],
);
const selectedErasedIdSet = useMemo(
() => new Set(erasedPointIds[selectedFan] || []),
[erasedPointIds, selectedFan],
);
const restoredFilteredPoints = useMemo(
() => selectedFiltered.filter((point) => selectedRestoredIdSet.has(point.point_id)),
() => selectedFiltered
.filter((point) => selectedRestoredIdSet.has(point.point_id))
.map((point) => ({ ...point, edit_origin: 'restored_filtered' })),
[selectedFiltered, selectedRestoredIdSet],
);
const effectiveBaseScatter = useMemo(
() => selectedScatter.filter((point) => !selectedErasedIdSet.has(point.point_id)),
[selectedScatter, selectedErasedIdSet],
);
const erasedScatterPoints = useMemo(
() => selectedScatter
.filter((point) => selectedErasedIdSet.has(point.point_id))
.map((point) => ({
...point,
reason: 'manual_erased',
edit_origin: 'erased_scatter',
})),
[selectedScatter, selectedErasedIdSet],
);
const visibleFilteredPoints = useMemo(
() => selectedFiltered.filter((point) => !selectedRestoredIdSet.has(point.point_id)),
[selectedFiltered, selectedRestoredIdSet],
() => [
...selectedFiltered.filter((point) => !selectedRestoredIdSet.has(point.point_id)),
...erasedScatterPoints,
],
[selectedFiltered, selectedRestoredIdSet, erasedScatterPoints],
);
const effectiveScatter = useMemo(
() => [...selectedScatter, ...restoredFilteredPoints],
[selectedScatter, restoredFilteredPoints],
() => [...effectiveBaseScatter, ...restoredFilteredPoints],
[effectiveBaseScatter, restoredFilteredPoints],
);
const selectedEstimatedParams = selectedFan && result?.estimated_params
? result.estimated_params[selectedFan]
@@ -760,11 +813,59 @@ export default function HomePage() {
});
}, [selectedFan]);
const handleErasePoints = useCallback((pointsToErase) => {
if (!selectedFan || !pointsToErase.length) return;
const restoredIdsToRemove = [];
const erasedIdsToAdd = [];
for (const point of pointsToErase) {
const pointId = point.point_id || createPointId(point, selectedFan);
if (point.edit_origin === 'restored_filtered') {
restoredIdsToRemove.push(pointId);
} else {
erasedIdsToAdd.push(pointId);
}
}
if (restoredIdsToRemove.length) {
setRestoredPointIds((prev) => {
const currentIds = new Set(prev[selectedFan] || []);
let changed = false;
for (const pointId of restoredIdsToRemove) {
if (currentIds.delete(pointId)) {
changed = true;
}
}
if (!changed) return prev;
return {
...prev,
[selectedFan]: Array.from(currentIds),
};
});
}
if (erasedIdsToAdd.length) {
setErasedPointIds((prev) => {
const nextIds = new Set(prev[selectedFan] || []);
let changed = false;
for (const pointId of erasedIdsToAdd) {
if (!nextIds.has(pointId)) {
nextIds.add(pointId);
changed = true;
}
}
if (!changed) return prev;
return {
...prev,
[selectedFan]: Array.from(nextIds),
};
});
}
}, [selectedFan]);
const handleToggleEditMode = useCallback(() => {
setIsEditMode((prev) => {
const next = !prev;
if (next) {
setShowFiltered(true);
setEditTool(EDIT_TOOL_RESTORE);
}
return next;
});
@@ -896,7 +997,9 @@ export default function HomePage() {
setSelectedFan('');
setShowFiltered(false);
setIsEditMode(false);
setEditTool(EDIT_TOOL_RESTORE);
setRestoredPointIds({});
setErasedPointIds({});
try {
const parsedFiles = [];
for (const file of selectedFiles) {
@@ -984,7 +1087,9 @@ export default function HomePage() {
setResult(null);
setShowFiltered(false);
setIsEditMode(false);
setEditTool(EDIT_TOOL_RESTORE);
setRestoredPointIds({});
setErasedPointIds({});
setProgress('正在准备标准化数据');
try {
@@ -1369,10 +1474,32 @@ export default function HomePage() {
className={`secondaryButton ${isEditMode ? 'active' : ''}`}
type="button"
onClick={handleToggleEditMode}
disabled={!selectedFiltered.length && !isEditMode}
disabled={!selectedFiltered.length && !effectiveScatter.length && !isEditMode}
>
{isEditMode ? '退出编辑' : '进入编辑'}
</button>
{isEditMode && (
<div className="editToolGroup" aria-label="编辑工具">
<button
className={`toolButton ${
editTool === EDIT_TOOL_RESTORE ? 'active' : ''
}`}
type="button"
onClick={() => setEditTool(EDIT_TOOL_RESTORE)}
>
笔刷
</button>
<button
className={`toolButton erase ${
editTool === EDIT_TOOL_ERASE ? 'active' : ''
}`}
type="button"
onClick={() => setEditTool(EDIT_TOOL_ERASE)}
>
橡皮擦
</button>
</div>
)}
<button
className="secondaryButton"
type="button"
@@ -1414,7 +1541,9 @@ export default function HomePage() {
showFiltered={showFiltered || isEditMode}
designPoints={designCurve}
editMode={isEditMode}
editTool={editTool}
onRestorePoints={handleRestorePoints}
onErasePoints={handleErasePoints}
/>
</div>