Skip to content

Commit 67ed3e6

Browse files
committed
feat: make cursor interaction work with nested components
1 parent 59d4c6b commit 67ed3e6

File tree

15 files changed

+142
-103
lines changed

15 files changed

+142
-103
lines changed

src/components/Angle.vue

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { useGraphContext } from "../composables/useGraphContext.ts";
3535
import { Color } from "../types.ts";
3636
import { useColors } from "../composables/useColors.ts";
3737
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
38-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
38+
import { useMatrices } from "../composables/useMatrices.ts";
3939
import { pointInsideSector } from "../utils/geometry.ts";
4040
import Label from "./Label.vue";
4141
@@ -62,14 +62,22 @@ const props = withDefaults(
6262
);
6363
6464
const { invScale } = useGraphContext();
65-
const matrix = useLocalToWorld(toRef(props, "position"));
65+
const { parentToWorld, cameraMatrix } = useMatrices(
66+
toRef(props, "position"),
67+
);
6668
const { parseColor } = useColors();
6769
6870
const stroke = parseColor(toRef(props, "color"), "stroke");
6971
const fill = parseColor(toRef(props, "fill"));
7072
const active = defineModel("active", { default: false });
7173
usePointerIntersection(active, (point) =>
72-
pointInsideSector(a.value, b.value, c.value, props.radius, point),
74+
pointInsideSector(
75+
a.value.transform(cameraMatrix.value),
76+
b.value.transform(cameraMatrix.value),
77+
c.value.transform(cameraMatrix.value),
78+
props.radius,
79+
point,
80+
),
7381
);
7482
7583
const a = computed(() => Vector2.wrap(props.a));
@@ -82,15 +90,15 @@ const sweep = computed(() => {
8290
c.value.x * (a.value.y - b.value.y);
8391
return orientation > 0 ? 1 : 0;
8492
});
85-
const scaledB = computed(() => b.value.transform(matrix.value));
86-
const scaledRadius = computed(() => props.radius * matrix.value.a);
93+
const scaledB = computed(() => b.value.transform(parentToWorld.value));
94+
const scaledRadius = computed(() => props.radius * parentToWorld.value.a);
8795
const start = computed(() => {
8896
const direction = a.value.sub(b.value).normalized();
89-
return b.value.add(direction.scale(props.radius)).transform(matrix.value);
97+
return b.value.add(direction.scale(props.radius)).transform(parentToWorld.value);
9098
});
9199
const end = computed(() => {
92100
const direction = c.value.sub(b.value).normalized();
93-
return b.value.add(direction.scale(props.radius)).transform(matrix.value);
101+
return b.value.add(direction.scale(props.radius)).transform(parentToWorld.value);
94102
});
95103
const dashArray = computed(() =>
96104
props.dashed ? [6 * invScale.value, 4 * invScale.value].join(",") : "0,0",

src/components/Arc.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Angle
33
v-bind="$attrs"
44
:a="a"
5-
:b="position"
5+
:b="0"
66
:c="c"
77
:radius="radius"
88
:dashed="dashed"
@@ -16,13 +16,14 @@
1616
</template>
1717

1818
<script setup lang="ts">
19-
import { computed } from "vue";
19+
import { computed, toRef } from "vue";
2020
2121
import { DEG2RAD } from "../utils/constants.ts";
2222
import Angle from "../components/Angle.vue";
2323
import { useColors } from "../composables/useColors.ts";
2424
import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts";
2525
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
26+
import { useMatrices } from "../composables/useMatrices.ts";
2627
import { distanceToArc } from "../utils/geometry.ts";
2728
2829
const props = withDefaults(
@@ -54,12 +55,13 @@ const props = withDefaults(
5455
const { colors } = useColors();
5556
const color = computed(() => props.color ?? colors.value.stroke);
5657
const active = defineModel("active", { default: false });
58+
const { cameraPosition } = useMatrices(toRef(props, "position"));
5759
usePointerIntersection(active, (point) => {
5860
const fromAngle = props.radians ? props.from : props.from * DEG2RAD;
5961
const toAngle = props.radians ? props.to : props.to * DEG2RAD;
6062
return (
6163
distanceToArc(
62-
Vector2.wrap(props.position),
64+
cameraPosition.value,
6365
fromAngle,
6466
toAngle,
6567
props.radius,
@@ -68,13 +70,12 @@ usePointerIntersection(active, (point) => {
6870
);
6971
});
7072
71-
const position = computed(() => Vector2.wrap(props.position));
7273
const a = computed(() => {
7374
const angle = props.radians ? props.from : props.from * DEG2RAD;
74-
return Vector2.fromAngle(angle).add(position.value);
75+
return Vector2.fromAngle(angle);
7576
});
7677
const c = computed(() => {
7778
const angle = props.radians ? props.to : props.to * DEG2RAD;
78-
return Vector2.fromAngle(angle).add(position.value);
79+
return Vector2.fromAngle(angle);
7980
});
8081
</script>

src/components/Circle.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { useGraphContext } from "../composables/useGraphContext.ts";
2121
import { type Color } from "../types.ts";
2222
import { useColors } from "../composables/useColors.ts";
2323
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
24-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
24+
import { useMatrices } from "../composables/useMatrices.ts";
2525
import { pointInsideCircle } from "../utils/geometry.ts";
2626
2727
const props = withDefaults(
@@ -41,20 +41,22 @@ const props = withDefaults(
4141
);
4242
4343
const { invScale } = useGraphContext();
44-
const matrix = useLocalToWorld(toRef(props, "position"));
44+
const { parentToWorld, cameraPosition } = useMatrices(
45+
toRef(props, "position"),
46+
);
4547
const { parseColor } = useColors();
4648
4749
const stroke = parseColor(toRef(props, "color"), "stroke");
4850
const fill = parseColor(toRef(props, "fill"));
4951
const active = defineModel("active", { default: false });
5052
usePointerIntersection(active, (point) =>
51-
pointInsideCircle(Vector2.wrap(props.position), props.radius, point),
53+
pointInsideCircle(cameraPosition.value, props.radius, point),
5254
);
5355
5456
const position = computed(() =>
55-
new Vector2(props.position).transform(matrix.value),
57+
new Vector2(props.position).transform(parentToWorld.value),
5658
);
57-
const scaledRadius = computed(() => props.radius * matrix.value.a);
59+
const scaledRadius = computed(() => props.radius * parentToWorld.value.a);
5860
const dashArray = computed(() =>
5961
props.dashed ? [6 * invScale.value, 4 * invScale.value].join(",") : "0,0",
6062
);

src/components/Ellipse.vue

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useGraphContext } from "../composables/useGraphContext.ts";
2323
import { type Color } from "../types.ts";
2424
import { useColors } from "../composables/useColors.ts";
2525
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
26-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
26+
import { useMatrices } from "../composables/useMatrices.ts";
2727
import { pointInsideEllipse } from "../utils/geometry.ts";
2828
2929
const props = withDefaults(
@@ -48,25 +48,24 @@ const props = withDefaults(
4848
);
4949
5050
const { invScale } = useGraphContext();
51-
const matrix = useLocalToWorld(toRef(props, "position"));
51+
const { parentToWorld, cameraPosition } = useMatrices(toRef(props, "position"));
5252
const { parseColor } = useColors();
5353
5454
const stroke = parseColor(toRef(props, "color"), "stroke");
5555
const fill = parseColor(toRef(props, "fill"));
5656
const active = defineModel("active", { default: false });
5757
usePointerIntersection(active, (point) =>
58-
pointInsideEllipse(
59-
Vector2.wrap(props.position),
60-
Vector2.wrap(props.radius),
61-
point,
62-
),
58+
pointInsideEllipse(cameraPosition.value, Vector2.wrap(props.radius), point),
6359
);
6460
6561
const position = computed(() =>
66-
new Vector2(props.position).transform(matrix.value),
62+
new Vector2(props.position).transform(parentToWorld.value),
6763
);
6864
const scaledRadius = computed(() =>
69-
new Vector2(props.radius).mul([matrix.value.a, Math.abs(matrix.value.d)]),
65+
new Vector2(props.radius).mul([
66+
parentToWorld.value.a,
67+
Math.abs(parentToWorld.value.d),
68+
]),
7069
);
7170
const dashArray = computed(() =>
7271
props.dashed ? [6 * invScale.value, 4 * invScale.value].join(",") : "0,0",

src/components/FunctionPlot.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { computed, onMounted, ref, toRef, watch } from "vue";
1212
1313
import { Color } from "../types.ts";
1414
import { useGraphContext } from "../composables/useGraphContext.ts";
15-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
15+
import { useMatrices } from "../composables/useMatrices.ts";
1616
import { PossibleVector2, Vector2 } from "../utils/Vector2.ts";
1717
import { useColors } from "../composables/useColors.ts";
1818
@@ -37,7 +37,7 @@ const props = withDefaults(
3737
let animationFrameID: number | null = null;
3838
3939
const { domain, size, invScale } = useGraphContext();
40-
const matrix = useLocalToWorld();
40+
const { parentToWorld } = useMatrices();
4141
const { parseColor } = useColors();
4242
4343
const color = parseColor(toRef(props, "color"), "stroke");
@@ -56,7 +56,7 @@ const visiblePoints = computed(() => {
5656
5757
const path = computed(() => {
5858
const points = visiblePoints.value.map((point) =>
59-
point.transform(matrix.value),
59+
point.transform(parentToWorld.value),
6060
);
6161
6262
if (points.length === 0) {

src/components/Group.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
<script lang="ts" setup>
88
import { toRef } from "vue";
99
import { PossibleVector2, Vector2 } from "../utils/Vector2.ts";
10-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
10+
import { useMatrices } from "../composables/useMatrices.ts";
1111
1212
const props = withDefaults(
1313
defineProps<{ position?: PossibleVector2; }>(), { position: () => new Vector2() });
1414
15-
useLocalToWorld(toRef(props, "position"));
15+
useMatrices(toRef(props, "position"));
1616
</script>

src/components/Label.vue

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { type PossibleVector2, Vector2 } from "../utils/Vector2.ts";
3535
import { useGraphContext } from "../composables/useGraphContext.ts";
3636
import { useColors } from "../composables/useColors.ts";
3737
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
38-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
38+
import { useMatrices } from "../composables/useMatrices.ts";
3939
import { pointInsideRectangle } from "../utils/geometry.ts";
4040
4141
const props = withDefaults(
@@ -68,20 +68,23 @@ const sizes = {
6868
};
6969
7070
const { invScale } = useGraphContext();
71-
const matrix = useLocalToWorld(toRef(props, "position"));
71+
const { parentToWorld, cameraPosition } = useMatrices(toRef(props, "position"));
7272
const { colors, parseColor } = useColors();
7373
7474
const color = parseColor(toRef(props, "color"), "stroke");
7575
const active = defineModel("active", { default: false });
7676
usePointerIntersection(active, (point) => {
77-
const center = Vector2.wrap(props.position);
78-
const width = boxWidth.value / matrix.value.a;
79-
const height = boxHeight.value / matrix.value.a;
80-
return pointInsideRectangle(center, new Vector2(width, height), point);
77+
const width = boxWidth.value / parentToWorld.value.a;
78+
const height = boxHeight.value / parentToWorld.value.a;
79+
return pointInsideRectangle(
80+
cameraPosition.value,
81+
new Vector2(width, height),
82+
point,
83+
);
8184
});
8285
8386
const position = computed(() =>
84-
Vector2.wrap(props.position).transform(matrix.value),
87+
Vector2.wrap(props.position).transform(parentToWorld.value),
8588
);
8689
const boxWidth = computed(() =>
8790
Math.max(

src/components/Line.vue

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
:stroke-width="lineWidth * invScale"
1010
:stroke-dasharray="dashArray"
1111
/>
12+
1213
<Label
1314
v-if="label"
1415
:text="label"
@@ -30,7 +31,7 @@ import Label from "./Label.vue";
3031
import { useGraphContext } from "../composables/useGraphContext.ts";
3132
import { useColors } from "../composables/useColors.ts";
3233
import { usePointerIntersection } from "../composables/usePointerIntersection.ts";
33-
import { useLocalToWorld } from "../composables/useLocalToWorld.ts";
34+
import { useMatrices } from "../composables/useMatrices.ts";
3435
import { distanceToLineSegment } from "../utils/geometry.ts";
3536
3637
const props = withDefaults(
@@ -61,50 +62,59 @@ if (props.to === undefined && props.slope === undefined) {
6162
6263
const { domain, invScale } = useGraphContext();
6364
const { parseColor } = useColors();
64-
const matrix = useLocalToWorld();
65+
const { parentToWorld, cameraMatrix } = useMatrices();
6566
6667
const color = parseColor(toRef(props, "color"), "stroke");
6768
const active = defineModel("active", { default: false });
68-
usePointerIntersection(
69-
active,
70-
(point) =>
69+
usePointerIntersection(active, (point) => {
70+
const matrix = cameraMatrix.value.multiply(parentToWorld.value.inverse);
71+
return (
7172
distanceToLineSegment(
72-
from.value.transform(matrix.value.inverse),
73-
to.value.transform(matrix.value.inverse),
73+
from.value.transform(matrix),
74+
to.value.transform(matrix),
7475
point,
75-
) <= props.highlightThreshold,
76-
);
76+
) <= props.highlightThreshold
77+
);
78+
});
7779
7880
function clamp(x: number, min: number, max: number) {
7981
return Math.min(max, Math.max(min, x));
8082
}
8183
8284
const from = computed(() => {
8385
if (props.from) {
84-
return new Vector2(props.from).transform(matrix.value);
86+
return new Vector2(props.from).transform(parentToWorld.value);
8587
}
8688
8789
if (props.to) {
88-
return new Vector2(0, 0).transform(matrix.value);
90+
return new Vector2(0, 0).transform(parentToWorld.value);
8991
}
9092
9193
let x = (domain.value.y.x - props.yIntercept) / props.slope!;
92-
x = clamp(x, domain.value.x.x, domain.value.x.y);
94+
x = clamp(
95+
x,
96+
domain.value.x.x + cameraMatrix.value.tx,
97+
domain.value.x.y - cameraMatrix.value.tx,
98+
);
9399
const y = props.slope! * x + props.yIntercept;
94-
return new Vector2(x, y).transform(matrix.value);
100+
return new Vector2(x, y).transform(parentToWorld.value);
95101
});
96102
const to = computed(() => {
97103
if (props.to) {
98-
return new Vector2(props.to).transform(matrix.value);
104+
return new Vector2(props.to).transform(parentToWorld.value);
99105
}
100106
101107
let x = (domain.value.y.y - props.yIntercept) / props.slope!;
102-
x = clamp(x, domain.value.x.x, domain.value.x.y);
108+
x = clamp(
109+
x,
110+
domain.value.x.x + cameraMatrix.value.tx,
111+
domain.value.x.y - cameraMatrix.value.tx,
112+
);
103113
const y = props.slope! * x + props.yIntercept;
104-
return new Vector2(x, y).transform(matrix.value);
114+
return new Vector2(x, y).transform(parentToWorld.value);
105115
});
106116
const labelPosition = computed(() => {
107-
const worldToLocal = matrix.value.inverse;
117+
const worldToLocal = parentToWorld.value.inverse;
108118
const localSpaceFrom = from.value.transform(worldToLocal);
109119
const localSpaceTo = to.value.transform(worldToLocal);
110120
const diff = localSpaceTo.sub(localSpaceFrom);

0 commit comments

Comments
 (0)