progress
This commit is contained in:
parent
145e61c607
commit
924019e81c
3 changed files with 122 additions and 53 deletions
|
|
@ -23,7 +23,7 @@ svg > text {
|
||||||
.rountangle:hover {
|
.rountangle:hover {
|
||||||
/* fill: lightgrey; */
|
/* fill: lightgrey; */
|
||||||
/* stroke-width: 4px; */
|
/* stroke-width: 4px; */
|
||||||
cursor: grab;
|
/* cursor: grab; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.rountangle.dragging {
|
.rountangle.dragging {
|
||||||
|
|
@ -38,6 +38,10 @@ svg > text {
|
||||||
stroke-width: 4px; */
|
stroke-width: 4px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected:hover {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
.lineHelper {
|
.lineHelper {
|
||||||
stroke: rgba(0, 0, 0, 0);
|
stroke: rgba(0, 0, 0, 0);
|
||||||
stroke-width: 16px;
|
stroke-width: 16px;
|
||||||
|
|
@ -87,3 +91,12 @@ line.selected, circle.selected {
|
||||||
stroke: blue;
|
stroke: blue;
|
||||||
stroke-width: 4px;
|
stroke-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text.selected, text.selected:hover {
|
||||||
|
fill: blue;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
}
|
||||||
|
text:hover {
|
||||||
|
fill: rgba(0, 200, 0, 1);
|
||||||
|
/* cursor: grab; */
|
||||||
|
}
|
||||||
|
|
@ -55,16 +55,20 @@ type RountanglePart = "left" | "top" | "right" | "bottom";
|
||||||
type ArrowPart = "start" | "end";
|
type ArrowPart = "start" | "end";
|
||||||
|
|
||||||
type RountangleSelectable = {
|
type RountangleSelectable = {
|
||||||
kind: "rountangle";
|
// kind: "rountangle";
|
||||||
parts: RountanglePart[];
|
parts: RountanglePart[];
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
}
|
||||||
type ArrowSelectable = {
|
type ArrowSelectable = {
|
||||||
kind: "arrow";
|
// kind: "arrow";
|
||||||
parts: ArrowPart[];
|
parts: ArrowPart[];
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
}
|
||||||
type Selectable = RountangleSelectable | ArrowSelectable;
|
type TextSelectable = {
|
||||||
|
parts: "text";
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
|
type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable;
|
||||||
type Selection = Selectable[];
|
type Selection = Selectable[];
|
||||||
|
|
||||||
const minStateSize = {x: 40, y: 40};
|
const minStateSize = {x: 40, y: 40};
|
||||||
|
|
@ -103,27 +107,53 @@ export function VisualEditor() {
|
||||||
|
|
||||||
|
|
||||||
const onMouseDown: MouseEventHandler<SVGSVGElement> = (e) => {
|
const onMouseDown: MouseEventHandler<SVGSVGElement> = (e) => {
|
||||||
console.log(e);
|
const currentPointer = {x: e.pageX, y: e.pageY};
|
||||||
const currentPointer = {x: e.clientX, y: e.clientY};
|
|
||||||
|
|
||||||
if (e.button === 1) {
|
if (e.button === 1) {
|
||||||
// ignore selection, middle mouse button always inserts
|
// ignore selection, middle mouse button always inserts
|
||||||
setState(state => {
|
setState(state => {
|
||||||
const newID = state.nextID.toString();
|
const newID = state.nextID.toString();
|
||||||
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
|
if (mode === "state") {
|
||||||
setDragging({
|
// insert rountangle
|
||||||
lastMousePos: currentPointer,
|
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
|
||||||
});
|
return {
|
||||||
return {
|
...state,
|
||||||
...state,
|
rountangles: [...state.rountangles, {
|
||||||
rountangles: [...state.rountangles, {
|
uid: newID,
|
||||||
uid: newID,
|
topLeft: currentPointer,
|
||||||
topLeft: currentPointer,
|
size: minStateSize,
|
||||||
size: minStateSize,
|
kind: "and",
|
||||||
kind: "and",
|
}],
|
||||||
}],
|
nextID: state.nextID+1,
|
||||||
nextID: state.nextID+1,
|
};
|
||||||
};
|
}
|
||||||
|
else if (mode === "transition") {
|
||||||
|
setSelection([{uid: newID, parts: ["end"]}]);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
arrows: [...state.arrows, {
|
||||||
|
uid: newID,
|
||||||
|
start: currentPointer,
|
||||||
|
end: currentPointer,
|
||||||
|
}],
|
||||||
|
nextID: state.nextID+1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (mode === "text") {
|
||||||
|
setSelection([{uid: newID, parts: ["text"]}]);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
texts: [...state.texts, {
|
||||||
|
uid: newID,
|
||||||
|
text: "Double-click to edit text",
|
||||||
|
topLeft: currentPointer,
|
||||||
|
}],
|
||||||
|
nextID: state.nextID+1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setDragging({
|
||||||
|
lastMousePos: currentPointer,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +190,7 @@ export function VisualEditor() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
const currentPointer = {x: e.clientX, y: e.clientY};
|
const currentPointer = {x: e.pageX, y: e.pageY};
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
setDragging(prevDragState => {
|
setDragging(prevDragState => {
|
||||||
const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
|
const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
|
||||||
|
|
@ -177,7 +207,8 @@ export function VisualEditor() {
|
||||||
kind: r.kind,
|
kind: r.kind,
|
||||||
...transformRect(r, parts, halfPointerDelta),
|
...transformRect(r, parts, halfPointerDelta),
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
|
.toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top
|
||||||
arrows: state.arrows.map(a => {
|
arrows: state.arrows.map(a => {
|
||||||
const parts = selection.find(selected => selected.uid === a.uid)?.parts || [];
|
const parts = selection.find(selected => selected.uid === a.uid)?.parts || [];
|
||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
|
|
@ -187,6 +218,17 @@ export function VisualEditor() {
|
||||||
uid: a.uid,
|
uid: a.uid,
|
||||||
...transformLine(a, parts, halfPointerDelta),
|
...transformLine(a, parts, halfPointerDelta),
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
texts: state.texts.map(t => {
|
||||||
|
const parts = selection.find(selected => selected.uid === t.uid)?.parts || [];
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uid: t.uid,
|
||||||
|
text: t.text,
|
||||||
|
topLeft: addV2D(t.topLeft, halfPointerDelta),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
return {lastMousePos: currentPointer};
|
return {lastMousePos: currentPointer};
|
||||||
|
|
@ -201,13 +243,6 @@ export function VisualEditor() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort: smaller rountangles are drawn on top
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
rountangles: state.rountangles.toSorted((a,b) => area(b) - area(a)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = (e: MouseEvent) => {
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
|
|
@ -217,7 +252,7 @@ export function VisualEditor() {
|
||||||
// we were making a selection
|
// we were making a selection
|
||||||
const normalizedSS = normalizeRect(ss);
|
const normalizedSS = normalizeRect(ss);
|
||||||
|
|
||||||
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle") || []) as SVGGraphicsElement[];
|
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
||||||
|
|
||||||
const shapesInSelection = shapes.filter(el => {
|
const shapesInSelection = shapes.filter(el => {
|
||||||
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
||||||
|
|
@ -244,12 +279,6 @@ export function VisualEditor() {
|
||||||
}
|
}
|
||||||
return null; // no longer selecting
|
return null; // no longer selecting
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort: smaller rountangles are drawn on top
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
rountangles: state.rountangles.toSorted((a,b) => area(b) - area(a)),
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
@ -259,17 +288,18 @@ export function VisualEditor() {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
|
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
|
||||||
|
arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)),
|
||||||
|
texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)),
|
||||||
}));
|
}));
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e.key === "o") {
|
if (e.key === "o") {
|
||||||
console.log('turn selected states into OR-states...');
|
|
||||||
// selected states become OR-states
|
// selected states become OR-states
|
||||||
setSelection(selection => {
|
setSelection(selection => {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => selection.includes(r.uid) ? ({...r, kind: "or"}) : r),
|
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r),
|
||||||
}));
|
}));
|
||||||
return selection;
|
return selection;
|
||||||
})
|
})
|
||||||
|
|
@ -279,7 +309,7 @@ export function VisualEditor() {
|
||||||
setSelection(selection => {
|
setSelection(selection => {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => selection.includes(r.uid) ? ({...r, kind: "and"}) : r),
|
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r),
|
||||||
}));
|
}));
|
||||||
return selection;
|
return selection;
|
||||||
});
|
});
|
||||||
|
|
@ -310,7 +340,7 @@ export function VisualEditor() {
|
||||||
};
|
};
|
||||||
}, [selectingState, dragging]);
|
}, [selectingState, dragging]);
|
||||||
|
|
||||||
return <svg width="100%" height="100%"
|
return <svg width="4000px" height="4000px"
|
||||||
className="svgCanvas"
|
className="svgCanvas"
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
|
|
@ -332,18 +362,48 @@ export function VisualEditor() {
|
||||||
{state.rountangles.map(rountangle => <RountangleSVG
|
{state.rountangles.map(rountangle => <RountangleSVG
|
||||||
key={rountangle.uid}
|
key={rountangle.uid}
|
||||||
rountangle={rountangle}
|
rountangle={rountangle}
|
||||||
dragging={(dragging!==null) && selection.includes(rountangle.uid)}
|
|
||||||
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
||||||
/>)}
|
/>)}
|
||||||
|
|
||||||
{state.arrows.map(arrow => <ArrowSVG
|
{state.arrows.map(arrow => <ArrowSVG
|
||||||
key={arrow.uid}
|
key={arrow.uid}
|
||||||
arrow={arrow}
|
arrow={arrow}
|
||||||
dragging={(dragging!==null) && selection.includes(arrow.uid)}
|
|
||||||
selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
|
selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{state.texts.map(txt => <text
|
||||||
|
key={txt.uid}
|
||||||
|
className={selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":""}
|
||||||
|
x={txt.topLeft.x}
|
||||||
|
width={200}
|
||||||
|
height={40}
|
||||||
|
y={txt.topLeft.y}
|
||||||
|
data-uid={txt.uid}
|
||||||
|
data-parts="text"
|
||||||
|
onDoubleClick={() => {
|
||||||
|
const newText = prompt("", txt.text);
|
||||||
|
if (newText) {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
texts: state.texts.map(t => {
|
||||||
|
if (t.uid === txt.uid) {
|
||||||
|
return {
|
||||||
|
...txt,
|
||||||
|
text: newText,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{txt.text}
|
||||||
|
</text>)}
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
|
|
||||||
{showHelp && <>
|
{showHelp && <>
|
||||||
|
|
@ -375,12 +435,11 @@ export function VisualEditor() {
|
||||||
const cornerOffset = 4;
|
const cornerOffset = 4;
|
||||||
const cornerRadius = 16;
|
const cornerRadius = 16;
|
||||||
|
|
||||||
export function RountangleSVG(props: {rountangle: Rountangle, dragging: boolean, selected: string[]}) {
|
export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]}) {
|
||||||
const {topLeft, size, uid} = props.rountangle;
|
const {topLeft, size, uid} = props.rountangle;
|
||||||
return <g transform={`translate(${topLeft.x} ${topLeft.y})`}>
|
return <g transform={`translate(${topLeft.x} ${topLeft.y})`}>
|
||||||
<rect
|
<rect
|
||||||
className={"rountangle"
|
className={"rountangle"
|
||||||
+(props.dragging?" dragging":"")
|
|
||||||
+(props.selected.length===4?" selected":"")
|
+(props.selected.length===4?" selected":"")
|
||||||
+((props.rountangle.kind==="or")?" or":"")}
|
+((props.rountangle.kind==="or")?" or":"")}
|
||||||
rx={20} ry={20}
|
rx={20} ry={20}
|
||||||
|
|
@ -474,14 +533,11 @@ export function RountangleSVG(props: {rountangle: Rountangle, dragging: boolean,
|
||||||
</g>;
|
</g>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ArrowSVG(props: {arrow: Arrow, dragging: boolean, selected: string[]}) {
|
export function ArrowSVG(props: {arrow: Arrow, selected: string[]}) {
|
||||||
const {start, end, uid} = props.arrow;
|
const {start, end, uid} = props.arrow;
|
||||||
return <g>
|
return <g>
|
||||||
<line
|
<line
|
||||||
className={"arrow"
|
className={"arrow"}
|
||||||
+(props.dragging?" dragging":"")
|
|
||||||
// +(props.selected.length===2?" selected":"")
|
|
||||||
}
|
|
||||||
markerEnd='url(#arrowEnd)'
|
markerEnd='url(#arrowEnd)'
|
||||||
x1={start.x}
|
x1={start.x}
|
||||||
y1={start.y}
|
y1={start.y}
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,12 @@ export function transformRect(rect: Rect2D, parts: string[], delta: Vec2D): Rect
|
||||||
y: parts.includes("top") ? rect.topLeft.y + delta.y : rect.topLeft.y,
|
y: parts.includes("top") ? rect.topLeft.y + delta.y : rect.topLeft.y,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
x: rect.size.x
|
x: Math.max(40, rect.size.x
|
||||||
+ (parts.includes("right") ? delta.x : 0)
|
+ (parts.includes("right") ? delta.x : 0)
|
||||||
- (parts.includes("left") ? delta.x : 0),
|
- (parts.includes("left") ? delta.x : 0)),
|
||||||
y: rect.size.y
|
y: Math.max(40, rect.size.y
|
||||||
+ (parts.includes("bottom") ? delta.y : 0)
|
+ (parts.includes("bottom") ? delta.y : 0)
|
||||||
- (parts.includes("top") ? delta.y : 0),
|
- (parts.includes("top") ? delta.y : 0)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue