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 @@
+
\ 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
new file mode 100644
index 000000000..e7b9f6305
--- /dev/null
+++ b/src/app/views/canvas/handles/BezierEditor.tsx
@@ -0,0 +1,386 @@
+/* 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";
+import * as R from "../../../resources";
+
+// The helper functions (getControlPoints, generatePathData) remain the same
+// as the previous example. They are included here for completeness.
+
+function generatePathData(points) {
+ if (!points || points.length < 4) {
+ return '';
+ }
+
+ let path = `M ${points[0].x} ${points[0].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;
+}
+
+export interface IBezierEditor {
+ points: Point[];
+ svgRef?: React.RefObject;
+ onChange: (points: { x: number, y: number }[], pathData: string, curve: Point[][]) => 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])
+
+ // TODO segments
+ const [points, setPoints] = useState(initialPoints);
+ const [symmetrical, setSymmetrical] = useState(true);
+
+ 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;
+ }
+ }, [initialPoints])
+
+ const handleMouseDown = (index) => {
+ setDraggingIndex(index);
+ };
+
+ const handleMouseUp = useCallback(() => {
+ setDraggingIndex(null);
+ }, []);
+
+ const handleMouseMove = useCallback((event) => {
+ if (draggingIndex === null || !svgRef.current) return;
+
+ console.log('mousemove', event.x, event.y, getPoint(event.x, event.y))
+ const newPoint = getPoint(event.x, event.y);
+
+ 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);
+ }, [draggingIndex, getPoint, setPoints, symmetrical, points]);
+
+ // 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: Math.random(), y: Math.random() });
+ newPoints.push({ x: Math.random(), y: Math.random() });
+ newPoints.push({ x: Math.random(), y: Math.random() });
+ return newPoints;
+ });
+ }}
+ >
+
+
+
+ );
+ }
+
+ const renderBezierControlRemoveButton = (x: number, y: number) => {
+ const margin = 2;
+ const cx = x - 16 - margin;
+ const cy = y + 16 + margin;
+ return (
+ {
+ if (points.length <= 4) {
+ return;
+ }
+ setPoints((prevPoints) => {
+ const newPoints = [...prevPoints];
+ newPoints.pop();
+ newPoints.pop();
+ 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));
+ let doubledPoint = [];
+ if (points.length > 4) {
+ // double anchor points
+ doubledPoint = points.flatMap((point, index) => {
+ if (index === 0 || index === points.length - 1) {
+ return point;
+ }
+ 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);
+ if (chunk.length == 4) {
+ chunks.push(chunk)
+ }
+ }
+
+ onChange(points, pathData, chunks);
+ }}
+ >
+
+
+
+ );
+ }
+
+ 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(
+ 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
+ )}
+ {/* 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))
+ )}
+
+
+
+ {/* 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..24e4b34a4 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,38 @@ 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 (
+ {
+ console.log('onChange', points, pathData)
+ this.setState({
+ points: points,
+ drawingCurve: false,
+ enabled: false,
+ });
+ 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 +233,77 @@ 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: [
+ { 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 }
+ ]
+ ] });
+ }}
+ >
+
+
+
+ );
+ }
+
+ 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,
+ });
+ }}
+ >
+
+
+
+ );
+ }
+
// eslint-disable-next-line
public renderSpiralButton(x: number, y: number) {
const margin = 2;
@@ -275,13 +391,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 +405,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 +445,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))
+ )}
+ {this.renderClearButton(
+ Math.max(this.fX(handle.x1), this.fX(handle.x2)),
+ Math.min(this.fY(handle.y1), this.fY(handle.y2)) + 38
)}
) : null}
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";