From c5165d530e8a530d9b868a7325186f3adff43593 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sat, 30 Aug 2025 01:58:31 -0800 Subject: [PATCH 1/9] BezierEditor --- src/app/views/canvas/handles/BezierEditor.tsx | 308 ++++++++++++++++++ src/app/views/canvas/handles/input_curve.tsx | 144 ++++++-- 2 files changed, 431 insertions(+), 21 deletions(-) create mode 100644 src/app/views/canvas/handles/BezierEditor.tsx diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx new file mode 100644 index 000000000..ca65d6a0b --- /dev/null +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -0,0 +1,308 @@ +/* eslint-disable max-lines-per-function */ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { Prototypes, Point, ZoomInfo } from "../../../../core"; +import * as R from "../../../resources"; + +// The helper functions (getControlPoints, generatePathData) remain the same +// as the previous example. They are included here for completeness. +const TENSION = 0.4; + +function getControlPoints(p0, p1, p2, p3) { + // const d1 = Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); + // const d2 = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + // const d3 = Math.sqrt(Math.pow(p3.x - p2.x, 2) + Math.pow(p3.y - p2.y, 2)); + + const cp1 = { + x: p1.x + (TENSION * (p2.x - p0.x)) / 6, + y: p1.y + (TENSION * (p2.y - p0.y)) / 6, + }; + const cp2 = { + x: p2.x - (TENSION * (p3.x - p1.x)) / 6, + y: p2.y - (TENSION * (p3.y - p1.y)) / 6, + }; + + return [cp1, cp2]; +} + +function generatePathData(points) { + if (points.length < 2) return ''; + let path = `M ${points[0].x} ${points[0].y}`; + const extendedPoints = [points[0], ...points, points[points.length - 1]]; + for (let i = 1; i < extendedPoints.length - 2; i++) { + const p0 = extendedPoints[i - 1]; + const p1 = extendedPoints[i]; + const p2 = extendedPoints[i + 1]; + const p3 = extendedPoints[i + 2]; + const [cp1, cp2] = getControlPoints(p0, p1, p2, p3); + path += ` C ${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${p2.x},${p2.y}`; + } + return path; +} + +export interface IBezierEditor { + points: Point[]; + svgRef?: React.RefObject; + onChange: (points: { x: number, y: number }[], pathData: string) => void; + x: number; + y: number; + width: number; + height: number; + handle: Prototypes.Handles.InputCurve; + zoom: ZoomInfo; +} + +// The main component updated with Hammer.js +// eslint-disable-next-line no-empty-pattern +export function BezierEditor({ handle, zoom, height, width, x, y, onChange, points: initialPoints }: IBezierEditor) { + + + const fX = (x: number) => + x * zoom.scale + zoom.centerX; + const fY = (y: number) => + -y * zoom.scale + zoom.centerY; + const transformPoint = (p: Point) => { + const scaler = Math.abs(handle.x2 - handle.x1) / 2; + const x = p.x * scaler + (handle.x1 + handle.x2) / 2; + const y = p.y * scaler + (handle.y1 + handle.y2) / 2; + return { + x: fX(x), + y: fY(y), + }; + }; + + const [draggingIndex, setDraggingIndex] = useState(null); + const svgRef = useRef(null); + + const getPoint = useCallback((x: number, y: number): Point => { + const bbox = svgRef.current.getBoundingClientRect(); + x -= bbox.left; + y -= bbox.top + bbox.height; + x /= zoom.scale; + y /= -zoom.scale; + // Scale x, y + const w = Math.abs(handle.x2 - handle.x1); + const h = Math.abs(handle.y2 - handle.y1); + return { + x: (x - w / 2) / (w / 2), + y: (y - h / 2) / (w / 2), + }; + }, [handle, zoom]) + + const [points, setPoints] = useState(initialPoints); + + // useEffect(() => { + // setPoints([ + // { + // x: -.5, + // y: -.5, + // }, + // { + // x: -.25, + // y: -.25, + // }, + // { + // x: 0, + // y: 0, + // }, + // { + // x: .25, + // y: .25, + // }, + // { + // x: .5, + // y: .5, + // } + // ]); + // }, []) + + const handleMouseDown = (index) => { + setDraggingIndex(index); + }; + + const handleMouseUp = useCallback(() => { + setDraggingIndex(null); + }, []); + + const handleMouseMove = useCallback((event) => { + if (draggingIndex === null || !svgRef.current) return; + + // const svgRect = svgRef.current.getBoundingClientRect(); + // const newX = event.clientX - svgRect.left; + // const newY = event.clientY - svgRect.top; + console.log('mousemove', event.x, event.y, getPoint(event.x, event.y)) + const newPoint = getPoint(event.x, event.y); + + setPoints((prevPoints) => { + const newPoints = [...prevPoints]; + newPoints[draggingIndex] = newPoint; + return newPoints; + }); + // onChange(points.map(transformPoint)); + }, [draggingIndex, getPoint, setPoints]); + + // Attach global mouse listeners for smoother dragging + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseMove, handleMouseUp]); + + const pathData = generatePathData(points.map(transformPoint)); + + const renderBezierControlAddButton = (x: number, y: number) => { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + setPoints((prevPoints) => { + const newPoints = [...prevPoints]; + newPoints.push({ x: 0, y: 0 }); + return newPoints; + }); + }} + > + + + + ); + } + + const renderBezierControlRemoveButton = (x: number, y: number) => { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + setPoints((prevPoints) => { + const newPoints = [...prevPoints]; + newPoints.pop(); + return newPoints; + }); + }} + > + + + + ); + } + + const renderBezierControlCloseButton = (x: number, y: number) => { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + const pathData = generatePathData(points.map(transformPoint)); + onChange(points, pathData); + }} + > + + + + ); + } + + return ( + <> + {renderBezierControlAddButton( + Math.max(fX(handle.x1), fX(handle.x2)), + Math.min(fY(handle.y1), fY(handle.y2)) + 38 + )} + {renderBezierControlRemoveButton( + Math.max(fX(handle.x1), fX(handle.x2)), + Math.min(fY(handle.y1), fY(handle.y2)) + 38 * 2 + )} + {renderBezierControlCloseButton( + Math.max(fX(handle.x1), fX(handle.x2)), + Math.min(fY(handle.y1), fY(handle.y2)) + )} + + + + {/* The Bezier Curve Path */} + + {/* The Draggable Anchor Points */} + + + {points.map((point, index) => { + const pt = transformPoint(point); + return ( + <> + handleMouseDown(index)} + /> + + ); + })} + + + ); +} + +export default BezierEditor; \ No newline at end of file diff --git a/src/app/views/canvas/handles/input_curve.tsx b/src/app/views/canvas/handles/input_curve.tsx index 891c8252b..4bd49dd67 100644 --- a/src/app/views/canvas/handles/input_curve.tsx +++ b/src/app/views/canvas/handles/input_curve.tsx @@ -12,12 +12,15 @@ import { HandlesDragContext, HandleViewProps } from "./common"; import { strings } from "../../../../strings"; import { FluentInputNumber } from "../../panels/widgets/controls/fluentui_input_number"; +import BezierEditor from "./BezierEditor"; + export interface InputCurveHandleViewProps extends HandleViewProps { handle: Prototypes.Handles.InputCurve; } export interface InputCurveHandleViewState { enabled: boolean; - drawing: boolean; + drawingPen: boolean; + drawingCurve: boolean; points: Point[]; } @@ -31,7 +34,8 @@ export class InputCurveHandleView extends React.Component< public state: InputCurveHandleViewState = { enabled: false, - drawing: false, + drawingPen: false, + drawingCurve: false, points: [], }; @@ -105,26 +109,35 @@ export class InputCurveHandleView extends React.Component< this.hammer = new Hammer(this.refs.interaction); this.hammer.on("panstart", (e) => { + if (this.state.drawingCurve) { + return; + } const x = e.center.x - e.deltaX; const y = e.center.y - e.deltaY; this.setState({ - drawing: true, + drawingPen: true, points: [this.getPoint(x, y)], }); }); this.hammer.on("pan", (e) => { + if (this.state.drawingCurve) { + return; + } this.state.points.push(this.getPoint(e.center.x, e.center.y)); this.setState({ points: this.state.points, }); }); this.hammer.on("panend", () => { + if (this.state.drawingCurve) { + return; + } const curve = this.getBezierCurvesFromMousePoints(this.state.points); const context = new HandlesDragContext(); this.props.onDragStart(this.props.handle, context); context.emit("end", { value: curve }); this.setState({ - drawing: false, + drawingPen: false, enabled: false, }); }); @@ -134,6 +147,39 @@ export class InputCurveHandleView extends React.Component< this.hammer.destroy(); } + public renderCurveDrawing() { + const handle = this.props.handle; + const fX = (x: number) => + x * this.props.zoom.scale + this.props.zoom.centerX; + const fY = (y: number) => + -y * this.props.zoom.scale + this.props.zoom.centerY; + return ( + { + debugger; + this.setState({ + points: points, + drawingCurve: false, + enabled: false, + }); + const curve = this.getBezierCurvesFromMousePoints(points); + const context = new HandlesDragContext(); + this.props.onDragStart(this.props.handle, context); + context.emit("end", { value: curve }); + }} + /> + ); + } + public renderDrawing() { const handle = this.props.handle; const fX = (x: number) => @@ -188,6 +234,55 @@ export class InputCurveHandleView extends React.Component< ); } + public renderBezierCurvesButton(x: number, y: number) { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + this.setState({ + enabled: true, + drawingCurve: true, + drawingPen: false, + // points: [ + // { + // x: -.5, + // y: -.5, + // }, + // { + // x: -.25, + // y: -.25, + // }, + // { + // x: 0, + // y: 0, + // }, + // { + // x: .25, + // y: .25, + // }, + // { + // x: .5, + // y: .5, + // } + // ] + }); + }} + > + + + + ); + } + // eslint-disable-next-line public renderSpiralButton(x: number, y: number) { const margin = 2; @@ -275,13 +370,13 @@ export class InputCurveHandleView extends React.Component< (a * (Math.cos(theta1) - theta1 * Math.sin(theta1))) / - scaler, + scaler, y: p1.y + (a * (Math.sin(theta1) + theta1 * Math.cos(theta1))) / - scaler, + scaler, }; const cp2 = { x: @@ -289,13 +384,13 @@ export class InputCurveHandleView extends React.Component< (a * (Math.cos(theta2) - theta2 * Math.sin(theta2))) / - scaler, + scaler, y: p2.y - (a * (Math.sin(theta2) + theta2 * Math.cos(theta2))) / - scaler, + scaler, }; curve.push([p1, cp1, cp2, p2].map(swapXY)); } @@ -329,12 +424,14 @@ export class InputCurveHandleView extends React.Component< ); } + private fX = (x: number) => + x * this.props.zoom.scale + this.props.zoom.centerX; + + private fY = (y: number) => + -y * this.props.zoom.scale + this.props.zoom.centerY; + public render() { const handle = this.props.handle; - const fX = (x: number) => - x * this.props.zoom.scale + this.props.zoom.centerX; - const fY = (y: number) => - -y * this.props.zoom.scale + this.props.zoom.centerY; return ( - {this.state.drawing ? this.renderDrawing() : null} + {this.state.drawingPen ? this.renderDrawing() : null} + {this.state.drawingCurve ? this.renderCurveDrawing() : null} {!this.state.enabled ? ( {this.renderSpiralButton( - Math.max(fX(handle.x1), fX(handle.x2)) - 38, - Math.min(fY(handle.y1), fY(handle.y2)) + Math.max(this.fX(handle.x1), this.fX(handle.x2)) - 76, + Math.min(this.fY(handle.y1), this.fY(handle.y2)) )} {this.renderButton( - Math.max(fX(handle.x1), fX(handle.x2)), - Math.min(fY(handle.y1), fY(handle.y2)) + Math.max(this.fX(handle.x1), this.fX(handle.x2)) - 38, + Math.min(this.fY(handle.y1), this.fY(handle.y2)) + )} + {this.renderBezierCurvesButton( + Math.max(this.fX(handle.x1), this.fX(handle.x2)), + Math.min(this.fY(handle.y1), this.fY(handle.y2)) )} ) : null} From 481a6253c4c84cf0651d5a6a4a03c16efeb20816 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 11:51:32 -0800 Subject: [PATCH 2/9] Cubic Bezier --- src/app/views/canvas/handles/BezierEditor.tsx | 204 ++++++++++++------ src/app/views/canvas/handles/input_curve.tsx | 61 +++--- 2 files changed, 177 insertions(+), 88 deletions(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index ca65d6a0b..39042ca56 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -1,3 +1,4 @@ +/* eslint-disable powerbi-visuals/insecure-random */ /* eslint-disable max-lines-per-function */ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Prototypes, Point, ZoomInfo } from "../../../../core"; @@ -7,34 +8,19 @@ import * as R from "../../../resources"; // as the previous example. They are included here for completeness. const TENSION = 0.4; -function getControlPoints(p0, p1, p2, p3) { - // const d1 = Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); - // const d2 = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); - // const d3 = Math.sqrt(Math.pow(p3.x - p2.x, 2) + Math.pow(p3.y - p2.y, 2)); - - const cp1 = { - x: p1.x + (TENSION * (p2.x - p0.x)) / 6, - y: p1.y + (TENSION * (p2.y - p0.y)) / 6, - }; - const cp2 = { - x: p2.x - (TENSION * (p3.x - p1.x)) / 6, - y: p2.y - (TENSION * (p3.y - p1.y)) / 6, - }; - - return [cp1, cp2]; -} - function generatePathData(points) { - if (points.length < 2) return ''; + if (!points || points.length < 4) { + return ''; + } + let path = `M ${points[0].x} ${points[0].y}`; - const extendedPoints = [points[0], ...points, points[points.length - 1]]; - for (let i = 1; i < extendedPoints.length - 2; i++) { - const p0 = extendedPoints[i - 1]; - const p1 = extendedPoints[i]; - const p2 = extendedPoints[i + 1]; - const p3 = extendedPoints[i + 2]; - const [cp1, cp2] = getControlPoints(p0, p1, p2, p3); - path += ` C ${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${p2.x},${p2.y}`; + for (let i = 1; i < points.length; i += 3) { + const p1 = points[i]; + const p2 = points[i + 1]; + const p3 = points[i + 2]; + if (p3) { // Ensure all points for a segment exist + path += ` C ${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`; + } } return path; } @@ -42,7 +28,7 @@ function generatePathData(points) { export interface IBezierEditor { points: Point[]; svgRef?: React.RefObject; - onChange: (points: { x: number, y: number }[], pathData: string) => void; + onChange: (points: { x: number, y: number }[], pathData: string, curve: Point[][]) => void; x: number; y: number; width: number; @@ -88,32 +74,23 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin }; }, [handle, zoom]) + // TODO segments const [points, setPoints] = useState(initialPoints); + const [symmetrical, setSymmetrical] = useState(true); - // useEffect(() => { - // setPoints([ - // { - // x: -.5, - // y: -.5, - // }, - // { - // x: -.25, - // y: -.25, - // }, - // { - // x: 0, - // y: 0, - // }, - // { - // x: .25, - // y: .25, - // }, - // { - // x: .5, - // y: .5, - // } - // ]); - // }, []) + useEffect(() => { + if (initialPoints.length <= 4) { + const extendedPoints = [...initialPoints] + for (let i = 0; i < 4 - initialPoints.length; i++) { + extendedPoints.push({ + x: Math.random(), + y: Math.random(), + }); + } + setPoints(extendedPoints); + return; + } + }, []) const handleMouseDown = (index) => { setDraggingIndex(index); @@ -126,19 +103,63 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin const handleMouseMove = useCallback((event) => { if (draggingIndex === null || !svgRef.current) return; - // const svgRect = svgRef.current.getBoundingClientRect(); - // const newX = event.clientX - svgRect.left; - // const newY = event.clientY - svgRect.top; console.log('mousemove', event.x, event.y, getPoint(event.x, event.y)) const newPoint = getPoint(event.x, event.y); - setPoints((prevPoints) => { - const newPoints = [...prevPoints]; - newPoints[draggingIndex] = newPoint; - return newPoints; - }); + const newPoints = [...points]; + + const oldPoint = newPoints[draggingIndex]; + + newPoints[draggingIndex] = { x: newPoint.x, y: newPoint.y }; + + if (symmetrical && draggingIndex % 3 !== 0) { + let anchorIndex; + let siblingIndex; + + // If dragging a 'right' control point (like P2, P5) + if ((draggingIndex + 1) % 3 === 0) { + anchorIndex = draggingIndex + 1; + siblingIndex = draggingIndex + 2; + } + // If dragging a 'left' control point (like P1, P4) + if ((draggingIndex - 1) % 3 === 0) { + anchorIndex = draggingIndex - 1; + siblingIndex = draggingIndex - 2; + } + + if (siblingIndex >= 0 && siblingIndex < newPoints.length) { + const anchorPoint = newPoints[anchorIndex]; + const draggedPoint = newPoints[draggingIndex]; + + // Reflect the sibling point through the anchor + const dx = anchorPoint.x - draggedPoint.x; + const dy = anchorPoint.y - draggedPoint.y; + + newPoints[siblingIndex] = { x: anchorPoint.x + dx, y: anchorPoint.y + dy }; + } + } + + // if dragging anchor point + if ((draggingIndex) % 3 === 0) { + const dx = newPoints[draggingIndex].x - oldPoint.x; + const dy = newPoints[draggingIndex].y - oldPoint.y; + + if (draggingIndex !== points.length - 1) { + newPoints[draggingIndex + 1] = { x: newPoints[draggingIndex + 1].x + dx, y: newPoints[draggingIndex + 1].y + dy }; + } + if (draggingIndex !== 0) { + newPoints[draggingIndex - 1] = { x: newPoints[draggingIndex - 1].x + dx, y: newPoints[draggingIndex - 1].y + dy }; + } + } + + setPoints(newPoints); + // setPoints((prevPoints) => { + // const newPoints = [...prevPoints]; + // newPoints[draggingIndex] = newPoint; + // return newPoints; + // }); // onChange(points.map(transformPoint)); - }, [draggingIndex, getPoint, setPoints]); + }, [draggingIndex, getPoint, setPoints, symmetrical, points]); // Attach global mouse listeners for smoother dragging useEffect(() => { @@ -163,7 +184,9 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin onClick={() => { setPoints((prevPoints) => { const newPoints = [...prevPoints]; - newPoints.push({ x: 0, y: 0 }); + newPoints.push({ x: Math.random(), y: Math.random() }); + newPoints.push({ x: Math.random(), y: Math.random() }); + newPoints.push({ x: Math.random(), y: Math.random() }); return newPoints; }); }} @@ -188,9 +211,14 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin { + if (points.length <= 4) { + return; + } setPoints((prevPoints) => { const newPoints = [...prevPoints]; newPoints.pop(); + newPoints.pop(); + newPoints.pop(); return newPoints; }); }} @@ -216,7 +244,31 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin className="handle-button" onClick={() => { const pathData = generatePathData(points.map(transformPoint)); - onChange(points, pathData); + let doubledPoint = []; + if (points.length > 4) { + doubledPoint = points.flatMap((point, index) => { + // double anchor points + if ((index) % 3 === 0) { + return [ + point, + point + ] + } + + return point; + }); + } else { + doubledPoint = points; + } + + const chunks: Point[][] = []; + const chunkSize = 4; + for (let i = 0; i < doubledPoint.length; i += chunkSize) { + const chunk = doubledPoint.slice(i, i + chunkSize); + chunks.push(chunk) + } + + onChange(points, pathData, chunks); }} > @@ -231,6 +283,29 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin ); } + const renderBezierControlSymmetryButton = (x: number, y: number) => { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + setSymmetrical((prev) => !prev); + }} + > + + + + ); + } + return ( <> {renderBezierControlAddButton( @@ -241,6 +316,11 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin Math.max(fX(handle.x1), fX(handle.x2)), Math.min(fY(handle.y1), fY(handle.y2)) + 38 * 2 )} + {/* ADD SYmmetrical checkbox */} + {renderBezierControlSymmetryButton( + Math.max(fX(handle.x1), fX(handle.x2)), + Math.min(fY(handle.y1), fY(handle.y2)) + 38 * 3 + )} {renderBezierControlCloseButton( Math.max(fX(handle.x1), fX(handle.x2)), Math.min(fY(handle.y1), fY(handle.y2)) diff --git a/src/app/views/canvas/handles/input_curve.tsx b/src/app/views/canvas/handles/input_curve.tsx index 4bd49dd67..dbcf8c2c8 100644 --- a/src/app/views/canvas/handles/input_curve.tsx +++ b/src/app/views/canvas/handles/input_curve.tsx @@ -160,18 +160,17 @@ export class InputCurveHandleView extends React.Component< width={Math.abs(fX(handle.x1) - fX(handle.x2))} height={Math.abs(fY(handle.y1) - fY(handle.y2))} - points={this.state.points.slice(1, 10)} + points={this.state.points} handle={handle} zoom={this.props.zoom} - onChange={(points, pathData) => { - debugger; + onChange={(points, pathData, curve: Point[][]) => { + console.log('onChange', points, pathData) this.setState({ points: points, drawingCurve: false, enabled: false, }); - const curve = this.getBezierCurvesFromMousePoints(points); const context = new HandlesDragContext(); this.props.onDragStart(this.props.handle, context); context.emit("end", { value: curve }); @@ -234,6 +233,34 @@ export class InputCurveHandleView extends React.Component< ); } + public renderClearButton(x: number, y: number) { + const margin = 2; + const cx = x - 16 - margin; + const cy = y + 16 + margin; + return ( + { + this.setState({ + points: [], + drawingCurve: false, + enabled: false, + drawingPen: false, + }); + }} + > + + + + ); + } + public renderBezierCurvesButton(x: number, y: number) { const margin = 2; const cx = x - 16 - margin; @@ -246,28 +273,6 @@ export class InputCurveHandleView extends React.Component< enabled: true, drawingCurve: true, drawingPen: false, - // points: [ - // { - // x: -.5, - // y: -.5, - // }, - // { - // x: -.25, - // y: -.25, - // }, - // { - // x: 0, - // y: 0, - // }, - // { - // x: .25, - // y: .25, - // }, - // { - // x: .5, - // y: .5, - // } - // ] }); }} > @@ -462,6 +467,10 @@ export class InputCurveHandleView extends React.Component< Math.max(this.fX(handle.x1), this.fX(handle.x2)), Math.min(this.fY(handle.y1), this.fY(handle.y2)) )} + {this.renderClearButton( + Math.max(this.fX(handle.x1), this.fX(handle.x2)), + Math.min(this.fY(handle.y1), this.fY(handle.y2)) + 38 + )} ) : null} From bf5b89d1629383e36c28519b4a00937a950cb507 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:11:30 -0800 Subject: [PATCH 3/9] Fix segmentation --- src/app/views/canvas/handles/BezierEditor.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index 39042ca56..24c27c7f0 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -246,8 +246,11 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin const pathData = generatePathData(points.map(transformPoint)); let doubledPoint = []; if (points.length > 4) { + // double anchor points doubledPoint = points.flatMap((point, index) => { - // double anchor points + if (index === 0 || index === points.length - 1) { + return point; + } if ((index) % 3 === 0) { return [ point, From da55b915847ad9ae89cb9b2e6158c48afbd3bea9 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:22:14 -0800 Subject: [PATCH 4/9] Exclude redundant chunk --- src/app/views/canvas/handles/BezierEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index 24c27c7f0..2beacf6b9 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -268,7 +268,9 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin const chunkSize = 4; for (let i = 0; i < doubledPoint.length; i += chunkSize) { const chunk = doubledPoint.slice(i, i + chunkSize); - chunks.push(chunk) + if (chunk.length == 4) { + chunks.push(chunk) + } } onChange(points, pathData, chunks); From 2584a83346d2047535fd87b4b60617e5ab0c1f46 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:25:11 -0800 Subject: [PATCH 5/9] Fix clear button --- src/app/views/canvas/handles/input_curve.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/views/canvas/handles/input_curve.tsx b/src/app/views/canvas/handles/input_curve.tsx index dbcf8c2c8..5097b708a 100644 --- a/src/app/views/canvas/handles/input_curve.tsx +++ b/src/app/views/canvas/handles/input_curve.tsx @@ -242,11 +242,27 @@ export class InputCurveHandleView extends React.Component< className="handle-button" onClick={() => { this.setState({ - points: [], + points: [ + { x: -.5, y: -.5 }, + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: .5, y: .5 } + ], drawingCurve: false, enabled: false, drawingPen: false, }); + + const context = new HandlesDragContext(); + this.props.onDragStart(this.props.handle, context); + context.emit("end", { value: [ + [ + { x: -1, y: -1 }, + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 1 } + ] + ] }); }} > From fcb402ce6b3ce796f3af3560fcac3979c8569177 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:42:48 -0800 Subject: [PATCH 6/9] lint --- src/app/views/canvas/handles/BezierEditor.tsx | 1 - src/app/views/panels/widgets/fluent_mapping_editor.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index 2beacf6b9..833e2af3e 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -6,7 +6,6 @@ import * as R from "../../../resources"; // The helper functions (getControlPoints, generatePathData) remain the same // as the previous example. They are included here for completeness. -const TENSION = 0.4; function generatePathData(points) { if (!points || points.length < 4) { diff --git a/src/app/views/panels/widgets/fluent_mapping_editor.tsx b/src/app/views/panels/widgets/fluent_mapping_editor.tsx index 2a240a349..6179e564e 100644 --- a/src/app/views/panels/widgets/fluent_mapping_editor.tsx +++ b/src/app/views/panels/widgets/fluent_mapping_editor.tsx @@ -12,7 +12,6 @@ import { Specification, } from "../../../../core"; import { DragData } from "../../../actions"; -import { ColorPicker } from "../../../components/fluentui_color_picker"; import { ContextedComponent } from "../../../context_component"; import { isKindAcceptable, type2DerivedColumns } from "../../dataset/common"; import { ScaleEditor } from "../scale_editor"; From 449981b6ad4bd46cb21ce28d94f9a54f5d4034c1 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:43:42 -0800 Subject: [PATCH 7/9] Remove commented code --- src/app/views/canvas/handles/BezierEditor.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index 833e2af3e..a0f0721da 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -152,12 +152,6 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin } setPoints(newPoints); - // setPoints((prevPoints) => { - // const newPoints = [...prevPoints]; - // newPoints[draggingIndex] = newPoint; - // return newPoints; - // }); - // onChange(points.map(transformPoint)); }, [draggingIndex, getPoint, setPoints, symmetrical, points]); // Attach global mouse listeners for smoother dragging From 5a777c7560a4fa393411d3cfc9fbb4f846578dae Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:44:21 -0800 Subject: [PATCH 8/9] Fix hook dependency --- src/app/views/canvas/handles/BezierEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index a0f0721da..da5287132 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -89,7 +89,7 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin setPoints(extendedPoints); return; } - }, []) + }, [initialPoints]) const handleMouseDown = (index) => { setDraggingIndex(index); From 9e6889d4b69a0866a790c487b2271ac3a9aaa4a4 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sun, 31 Aug 2025 21:59:00 -0800 Subject: [PATCH 9/9] Update icons --- resources/icons/icons_unbind-data .svg | 10 ++++++++++ src/app/resources/icons.ts | 1 + src/app/views/canvas/handles/BezierEditor.tsx | 2 +- src/app/views/canvas/handles/input_curve.tsx | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 resources/icons/icons_unbind-data .svg diff --git a/resources/icons/icons_unbind-data .svg b/resources/icons/icons_unbind-data .svg new file mode 100644 index 000000000..9becfa679 --- /dev/null +++ b/resources/icons/icons_unbind-data .svg @@ -0,0 +1,10 @@ + + icons + + + \ No newline at end of file diff --git a/src/app/resources/icons.ts b/src/app/resources/icons.ts index f15e5fe00..9021262c2 100644 --- a/src/app/resources/icons.ts +++ b/src/app/resources/icons.ts @@ -39,6 +39,7 @@ addSVGIcon("general/dropdown", require("resources/icons/icons_dropdown.svg")); addSVGIcon("Edit", require("resources/icons/icons_edit.svg")); addSVGIcon("general/eraser", require("resources/icons/icons_eraser.svg")); addSVGIcon("general/bind-data", require("resources/icons/icons_bind-data.svg")); +addSVGIcon("general/unbind-data", require("resources/icons/icons_unbind-data .svg")); addSVGIcon("general/confirm", require("resources/icons/icons_confirm.svg")); addSVGIcon( diff --git a/src/app/views/canvas/handles/BezierEditor.tsx b/src/app/views/canvas/handles/BezierEditor.tsx index da5287132..e7b9f6305 100644 --- a/src/app/views/canvas/handles/BezierEditor.tsx +++ b/src/app/views/canvas/handles/BezierEditor.tsx @@ -294,7 +294,7 @@ export function BezierEditor({ handle, zoom, height, width, x, y, onChange, poin >