加个橡皮擦
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"document_root": "./web",
|
"document_root": "./web",
|
||||||
"upload_path": "uploads",
|
"upload_path": "uploads",
|
||||||
"client_max_body_size": "10M",
|
"client_max_body_size": "10M",
|
||||||
|
"client_max_memory_body_size": "20M",
|
||||||
"max_connections": 1000,
|
"max_connections": 1000,
|
||||||
"log": {
|
"log": {
|
||||||
"log_path": "",
|
"log_path": "",
|
||||||
|
|||||||
@@ -75,6 +75,45 @@
|
|||||||
color: #fde68a;
|
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 {
|
.uploadButton input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -607,6 +646,16 @@ select:focus {
|
|||||||
background: rgba(251, 191, 36, 0.16);
|
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 {
|
.chartTools {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ const DESIGN_FIELD_HINTS = {
|
|||||||
|
|
||||||
const CHUNK_SIZE = 4000;
|
const CHUNK_SIZE = 4000;
|
||||||
const CHART_HEIGHT = 540;
|
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 DEFAULT_SCHEME_ID = 'scheme_one';
|
||||||
const SCHEME_TWO_ID = 'scheme_two';
|
const SCHEME_TWO_ID = 'scheme_two';
|
||||||
const DEFAULT_SCHEMES = [
|
const DEFAULT_SCHEMES = [
|
||||||
@@ -436,7 +438,9 @@ function PowerCurveChart({
|
|||||||
showFiltered,
|
showFiltered,
|
||||||
designPoints,
|
designPoints,
|
||||||
editMode,
|
editMode,
|
||||||
|
editTool,
|
||||||
onRestorePoints,
|
onRestorePoints,
|
||||||
|
onErasePoints,
|
||||||
}) {
|
}) {
|
||||||
const frameRef = useRef(null);
|
const frameRef = useRef(null);
|
||||||
const chartRef = useRef(null);
|
const chartRef = useRef(null);
|
||||||
@@ -587,14 +591,17 @@ function PowerCurveChart({
|
|||||||
dragging: nextDragging,
|
dragging: nextDragging,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const restoreAt = (event) => {
|
const applyToolAt = (event) => {
|
||||||
if (!chart.over || !onRestorePoints) return;
|
if (!chart.over) return;
|
||||||
const rect = chart.over.getBoundingClientRect();
|
const rect = chart.over.getBoundingClientRect();
|
||||||
const x = event.clientX - rect.left;
|
const x = event.clientX - rect.left;
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const radiusSquared = BRUSH_RADIUS * BRUSH_RADIUS;
|
const radiusSquared = BRUSH_RADIUS * BRUSH_RADIUS;
|
||||||
const restored = [];
|
const targetPoints = editTool === EDIT_TOOL_ERASE
|
||||||
for (const point of validFiltered) {
|
? validScatter
|
||||||
|
: validFiltered.filter((point) => point.reason !== 'manual_erased');
|
||||||
|
const hitPoints = [];
|
||||||
|
for (const point of targetPoints) {
|
||||||
const pointX = chart.valToPos(point.wind_speed, 'x');
|
const pointX = chart.valToPos(point.wind_speed, 'x');
|
||||||
const pointY = chart.valToPos(point.active_power, 'y');
|
const pointY = chart.valToPos(point.active_power, 'y');
|
||||||
if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) {
|
if (!Number.isFinite(pointX) || !Number.isFinite(pointY)) {
|
||||||
@@ -603,11 +610,16 @@ function PowerCurveChart({
|
|||||||
const dx = pointX - x;
|
const dx = pointX - x;
|
||||||
const dy = pointY - y;
|
const dy = pointY - y;
|
||||||
if (dx * dx + dy * dy <= radiusSquared) {
|
if (dx * dx + dy * dy <= radiusSquared) {
|
||||||
restored.push(point);
|
hitPoints.push(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (restored.length) {
|
if (!hitPoints.length) {
|
||||||
onRestorePoints(restored);
|
return;
|
||||||
|
}
|
||||||
|
if (editTool === EDIT_TOOL_ERASE) {
|
||||||
|
onErasePoints?.(hitPoints);
|
||||||
|
} else {
|
||||||
|
onRestorePoints?.(hitPoints);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handlePointerDown = (event) => {
|
const handlePointerDown = (event) => {
|
||||||
@@ -615,12 +627,12 @@ function PowerCurveChart({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dragging = true;
|
dragging = true;
|
||||||
updateBrush(event, true);
|
updateBrush(event, true);
|
||||||
restoreAt(event);
|
applyToolAt(event);
|
||||||
};
|
};
|
||||||
const handlePointerMove = (event) => {
|
const handlePointerMove = (event) => {
|
||||||
updateBrush(event);
|
updateBrush(event);
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
restoreAt(event);
|
applyToolAt(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handlePointerUp = (event) => {
|
const handlePointerUp = (event) => {
|
||||||
@@ -647,7 +659,9 @@ function PowerCurveChart({
|
|||||||
validDesign,
|
validDesign,
|
||||||
showFiltered,
|
showFiltered,
|
||||||
editMode,
|
editMode,
|
||||||
|
editTool,
|
||||||
onRestorePoints,
|
onRestorePoints,
|
||||||
|
onErasePoints,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -661,7 +675,12 @@ function PowerCurveChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`chartFrame ${editMode ? 'chartFrameEditing' : ''}`} ref={frameRef}>
|
<div
|
||||||
|
className={`chartFrame ${editMode ? 'chartFrameEditing' : ''} ${
|
||||||
|
editTool === EDIT_TOOL_ERASE ? 'chartFrameErasing' : ''
|
||||||
|
}`}
|
||||||
|
ref={frameRef}
|
||||||
|
>
|
||||||
<div className="chartLegend">
|
<div className="chartLegend">
|
||||||
<span><i className="legendDot scatter" />散点数据</span>
|
<span><i className="legendDot scatter" />散点数据</span>
|
||||||
<span><i className="legendLine actual" />实际功率</span>
|
<span><i className="legendLine actual" />实际功率</span>
|
||||||
@@ -670,7 +689,9 @@ function PowerCurveChart({
|
|||||||
<div className="uplotWrap" ref={chartRef} />
|
<div className="uplotWrap" ref={chartRef} />
|
||||||
{editMode && brush.visible && (
|
{editMode && brush.visible && (
|
||||||
<div
|
<div
|
||||||
className={`chartBrush ${brush.dragging ? 'dragging' : ''}`}
|
className={`chartBrush ${editTool === EDIT_TOOL_ERASE ? 'erase' : ''} ${
|
||||||
|
brush.dragging ? 'dragging' : ''
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: BRUSH_RADIUS * 2,
|
width: BRUSH_RADIUS * 2,
|
||||||
height: BRUSH_RADIUS * 2,
|
height: BRUSH_RADIUS * 2,
|
||||||
@@ -693,7 +714,9 @@ export default function HomePage() {
|
|||||||
const [selectedFan, setSelectedFan] = useState('');
|
const [selectedFan, setSelectedFan] = useState('');
|
||||||
const [showFiltered, setShowFiltered] = useState(false);
|
const [showFiltered, setShowFiltered] = useState(false);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
const [editTool, setEditTool] = useState(EDIT_TOOL_RESTORE);
|
||||||
const [restoredPointIds, setRestoredPointIds] = useState({});
|
const [restoredPointIds, setRestoredPointIds] = useState({});
|
||||||
|
const [erasedPointIds, setErasedPointIds] = useState({});
|
||||||
const [readingDesign, setReadingDesign] = useState(false);
|
const [readingDesign, setReadingDesign] = useState(false);
|
||||||
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
||||||
const [designMeta, setDesignMeta] = useState(null);
|
const [designMeta, setDesignMeta] = useState(null);
|
||||||
@@ -710,9 +733,16 @@ export default function HomePage() {
|
|||||||
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
|
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
|
||||||
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
||||||
const selectedBins = selectedFan && result?.bins ? result.bins[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] || []
|
? result.scatter_points[selectedFan] || []
|
||||||
: [];
|
: [];
|
||||||
|
const selectedScatter = useMemo(
|
||||||
|
() => withPointIds(selectedScatterRaw, selectedFan).map((point) => ({
|
||||||
|
...point,
|
||||||
|
edit_origin: 'scatter',
|
||||||
|
})),
|
||||||
|
[selectedScatterRaw, selectedFan],
|
||||||
|
);
|
||||||
const selectedFilteredRaw = selectedFan && result?.filtered_points
|
const selectedFilteredRaw = selectedFan && result?.filtered_points
|
||||||
? result.filtered_points[selectedFan] || []
|
? result.filtered_points[selectedFan] || []
|
||||||
: [];
|
: [];
|
||||||
@@ -724,17 +754,40 @@ export default function HomePage() {
|
|||||||
() => new Set(restoredPointIds[selectedFan] || []),
|
() => new Set(restoredPointIds[selectedFan] || []),
|
||||||
[restoredPointIds, selectedFan],
|
[restoredPointIds, selectedFan],
|
||||||
);
|
);
|
||||||
|
const selectedErasedIdSet = useMemo(
|
||||||
|
() => new Set(erasedPointIds[selectedFan] || []),
|
||||||
|
[erasedPointIds, selectedFan],
|
||||||
|
);
|
||||||
const restoredFilteredPoints = useMemo(
|
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],
|
[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(
|
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(
|
const effectiveScatter = useMemo(
|
||||||
() => [...selectedScatter, ...restoredFilteredPoints],
|
() => [...effectiveBaseScatter, ...restoredFilteredPoints],
|
||||||
[selectedScatter, restoredFilteredPoints],
|
[effectiveBaseScatter, restoredFilteredPoints],
|
||||||
);
|
);
|
||||||
const selectedEstimatedParams = selectedFan && result?.estimated_params
|
const selectedEstimatedParams = selectedFan && result?.estimated_params
|
||||||
? result.estimated_params[selectedFan]
|
? result.estimated_params[selectedFan]
|
||||||
@@ -760,11 +813,59 @@ export default function HomePage() {
|
|||||||
});
|
});
|
||||||
}, [selectedFan]);
|
}, [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(() => {
|
const handleToggleEditMode = useCallback(() => {
|
||||||
setIsEditMode((prev) => {
|
setIsEditMode((prev) => {
|
||||||
const next = !prev;
|
const next = !prev;
|
||||||
if (next) {
|
if (next) {
|
||||||
setShowFiltered(true);
|
setShowFiltered(true);
|
||||||
|
setEditTool(EDIT_TOOL_RESTORE);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -896,7 +997,9 @@ export default function HomePage() {
|
|||||||
setSelectedFan('');
|
setSelectedFan('');
|
||||||
setShowFiltered(false);
|
setShowFiltered(false);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
|
setEditTool(EDIT_TOOL_RESTORE);
|
||||||
setRestoredPointIds({});
|
setRestoredPointIds({});
|
||||||
|
setErasedPointIds({});
|
||||||
try {
|
try {
|
||||||
const parsedFiles = [];
|
const parsedFiles = [];
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
@@ -984,7 +1087,9 @@ export default function HomePage() {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
setShowFiltered(false);
|
setShowFiltered(false);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
|
setEditTool(EDIT_TOOL_RESTORE);
|
||||||
setRestoredPointIds({});
|
setRestoredPointIds({});
|
||||||
|
setErasedPointIds({});
|
||||||
setProgress('正在准备标准化数据');
|
setProgress('正在准备标准化数据');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1369,10 +1474,32 @@ export default function HomePage() {
|
|||||||
className={`secondaryButton ${isEditMode ? 'active' : ''}`}
|
className={`secondaryButton ${isEditMode ? 'active' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleEditMode}
|
onClick={handleToggleEditMode}
|
||||||
disabled={!selectedFiltered.length && !isEditMode}
|
disabled={!selectedFiltered.length && !effectiveScatter.length && !isEditMode}
|
||||||
>
|
>
|
||||||
{isEditMode ? '退出编辑' : '进入编辑'}
|
{isEditMode ? '退出编辑' : '进入编辑'}
|
||||||
</button>
|
</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
|
<button
|
||||||
className="secondaryButton"
|
className="secondaryButton"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1414,7 +1541,9 @@ export default function HomePage() {
|
|||||||
showFiltered={showFiltered || isEditMode}
|
showFiltered={showFiltered || isEditMode}
|
||||||
designPoints={designCurve}
|
designPoints={designCurve}
|
||||||
editMode={isEditMode}
|
editMode={isEditMode}
|
||||||
|
editTool={editTool}
|
||||||
onRestorePoints={handleRestorePoints}
|
onRestorePoints={handleRestorePoints}
|
||||||
|
onErasePoints={handleErasePoints}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user