fix: add anchor for station
This commit is contained in:
@@ -364,16 +364,19 @@ const computeViewTransform = (
|
|||||||
return { scale, translation };
|
return { scale, translation };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAnchorFromOffset = (
|
const getAnchorFromOffset = (align: number): { x: number; y: number } => {
|
||||||
offsetX: number,
|
let anchorX: number;
|
||||||
offsetY: number
|
if (align === 1) {
|
||||||
): { x: number; y: number } => {
|
anchorX = 0;
|
||||||
const length = Math.hypot(offsetX, offsetY);
|
} else if (align === 3) {
|
||||||
|
anchorX = 1;
|
||||||
|
} else {
|
||||||
|
anchorX = 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
const nx = offsetX / length;
|
const anchorY = 0.5;
|
||||||
const ny = offsetY / length;
|
|
||||||
|
|
||||||
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
return { x: anchorX, y: anchorY };
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundColor = toColor(BACKGROUND_COLOR);
|
const backgroundColor = toColor(BACKGROUND_COLOR);
|
||||||
@@ -665,24 +668,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
next: Transform,
|
next: Transform,
|
||||||
options?: { immediate?: boolean; skipClamp?: boolean }
|
options?: { immediate?: boolean; skipClamp?: boolean }
|
||||||
) => {
|
) => {
|
||||||
console.log("🔄 updateTransform вызван", {
|
|
||||||
inputTransform: next,
|
|
||||||
options,
|
|
||||||
stackTrace: new Error().stack?.split("\n").slice(1, 4).join("\n"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const adjusted = options?.skipClamp ? next : clampTransformScale(next);
|
const adjusted = options?.skipClamp ? next : clampTransformScale(next);
|
||||||
|
|
||||||
if (adjusted !== next && !options?.skipClamp) {
|
|
||||||
console.log(
|
|
||||||
"🔄 updateTransform: transform изменен после clampTransformScale",
|
|
||||||
{
|
|
||||||
before: next,
|
|
||||||
after: adjusted,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
transformRef.current = adjusted;
|
transformRef.current = adjusted;
|
||||||
if (options?.immediate) {
|
if (options?.immediate) {
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
@@ -732,18 +719,21 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const world = getWorldPosition(
|
const stationScreenX =
|
||||||
event.clientX,
|
state.rotatedBase.x * state.camera.scale + state.camera.translation.x;
|
||||||
event.clientY,
|
const stationScreenY =
|
||||||
state.camera
|
state.rotatedBase.y * state.camera.scale + state.camera.translation.y;
|
||||||
);
|
|
||||||
if (!world) return;
|
|
||||||
|
|
||||||
const adjustedWorldX = world.x - state.pointerDelta.x;
|
const canvas = canvasRef.current;
|
||||||
const adjustedWorldY = world.y - state.pointerDelta.y;
|
if (!canvas) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / Math.max(rect.width, 1);
|
||||||
|
const scaleY = canvas.height / Math.max(rect.height, 1);
|
||||||
|
const pointerScreenX = (event.clientX - rect.left) * scaleX;
|
||||||
|
const pointerScreenY = (event.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
const newOffsetX = adjustedWorldX - state.rotatedBase.x;
|
const newOffsetX = pointerScreenX - stationScreenX - state.pointerDelta.x;
|
||||||
const newOffsetY = adjustedWorldY - state.rotatedBase.y;
|
const newOffsetY = pointerScreenY - stationScreenY - state.pointerDelta.y;
|
||||||
|
|
||||||
state.lastOffset = { x: newOffsetX, y: newOffsetY };
|
state.lastOffset = { x: newOffsetX, y: newOffsetY };
|
||||||
setLiveStationOffsets((prev) => {
|
setLiveStationOffsets((prev) => {
|
||||||
@@ -810,19 +800,25 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
|
|
||||||
suppressAutoFitRef.current = true;
|
suppressAutoFitRef.current = true;
|
||||||
|
|
||||||
const pointerWorld = getWorldPosition(
|
const stationScreenX =
|
||||||
event.clientX,
|
rotatedBase.x * camera.scale + camera.translation.x;
|
||||||
event.clientY,
|
const stationScreenY =
|
||||||
camera
|
rotatedBase.y * camera.scale + camera.translation.y;
|
||||||
);
|
const labelScreenX = stationScreenX + currentOffset.x;
|
||||||
const labelWorldX = rotatedBase.x + currentOffset.x;
|
const labelScreenY = stationScreenY + currentOffset.y;
|
||||||
const labelWorldY = rotatedBase.y + currentOffset.y;
|
|
||||||
const pointerDelta = pointerWorld
|
const canvas = canvasRef.current;
|
||||||
? {
|
if (!canvas) return;
|
||||||
x: pointerWorld.x - labelWorldX,
|
const rect = canvas.getBoundingClientRect();
|
||||||
y: pointerWorld.y - labelWorldY,
|
const scaleX = canvas.width / Math.max(rect.width, 1);
|
||||||
}
|
const scaleY = canvas.height / Math.max(rect.height, 1);
|
||||||
: { x: 0, y: 0 };
|
const pointerScreenX = (event.clientX - rect.left) * scaleX;
|
||||||
|
const pointerScreenY = (event.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const pointerDelta = {
|
||||||
|
x: pointerScreenX - labelScreenX,
|
||||||
|
y: pointerScreenY - labelScreenY,
|
||||||
|
};
|
||||||
|
|
||||||
const captureTarget = event.currentTarget;
|
const captureTarget = event.currentTarget;
|
||||||
if (captureTarget.setPointerCapture) {
|
if (captureTarget.setPointerCapture) {
|
||||||
@@ -1050,7 +1046,6 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
||||||
|
|
||||||
if (!(gl instanceof WebGLRenderingContext)) {
|
if (!(gl instanceof WebGLRenderingContext)) {
|
||||||
console.error("WebGL is not supported in this browser");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1074,7 +1069,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
lineBufferRef.current = gl.createBuffer();
|
lineBufferRef.current = gl.createBuffer();
|
||||||
pointBufferRef.current = gl.createBuffer();
|
pointBufferRef.current = gl.createBuffer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize WebGL", error);
|
// console.error("Failed to initialize WebGL", error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -1912,17 +1907,23 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
? liveStationOffset.y
|
? liveStationOffset.y
|
||||||
: baseOffsetY;
|
: baseOffsetY;
|
||||||
|
|
||||||
const labelX =
|
const stationScreenX =
|
||||||
(rotatedX + offsetX) * camera.scale + camera.translation.x;
|
rotatedX * camera.scale + camera.translation.x;
|
||||||
const labelY =
|
const stationScreenY =
|
||||||
(rotatedY + offsetY) * camera.scale + camera.translation.y;
|
rotatedY * camera.scale + camera.translation.y;
|
||||||
|
|
||||||
const anchor = getAnchorFromOffset(offsetX, offsetY);
|
const labelX = stationScreenX + offsetX;
|
||||||
|
const labelY = stationScreenY + offsetY;
|
||||||
|
|
||||||
|
const backendAlign = station.align;
|
||||||
|
|
||||||
|
const anchor = getAnchorFromOffset(backendAlign ?? 2);
|
||||||
const transformCss = `translate(${-anchor.x * 100}%, ${
|
const transformCss = `translate(${-anchor.x * 100}%, ${
|
||||||
-anchor.y * 100
|
-anchor.y * 100
|
||||||
}%)`;
|
}%)`;
|
||||||
|
|
||||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
|
|
||||||
const cssX = labelX / dpr;
|
const cssX = labelX / dpr;
|
||||||
const cssY = labelY / dpr;
|
const cssY = labelY / dpr;
|
||||||
const rotationCss = `${rotationAngle}rad`;
|
const rotationCss = `${rotationAngle}rad`;
|
||||||
@@ -1943,7 +1944,6 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
|
|
||||||
const secondaryMarginTop = 5 * fontScale;
|
const secondaryMarginTop = 5 * fontScale;
|
||||||
|
|
||||||
const backendAlign = station.align;
|
|
||||||
const alignmentFromData: StationAlignment =
|
const alignmentFromData: StationAlignment =
|
||||||
backendAlign === 1
|
backendAlign === 1
|
||||||
? "left"
|
? "left"
|
||||||
@@ -1969,151 +1969,154 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
: 3;
|
: 3;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={station.id}>
|
||||||
key={station.id}
|
|
||||||
onMouseEnter={() => setHoveredStationId(station.id)}
|
|
||||||
onMouseLeave={() =>
|
|
||||||
setHoveredStationId((prev) =>
|
|
||||||
prev === station.id ? null : prev
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPointerDown={(event) =>
|
|
||||||
handleStationPointerDown(
|
|
||||||
event,
|
|
||||||
station.id,
|
|
||||||
{
|
|
||||||
x: rotatedX,
|
|
||||||
y: rotatedY,
|
|
||||||
},
|
|
||||||
{ x: offsetX, y: offsetY }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: cssX,
|
|
||||||
top: cssY,
|
|
||||||
transform: transformCss,
|
|
||||||
color: "#fff",
|
|
||||||
fontFamily: "Roboto, sans-serif",
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
cursor: "grab",
|
|
||||||
userSelect: "none",
|
|
||||||
touchAction: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={() => setHoveredStationId(station.id)}
|
||||||
|
onMouseLeave={() =>
|
||||||
|
setHoveredStationId((prev) =>
|
||||||
|
prev === station.id ? null : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPointerDown={(event) =>
|
||||||
|
handleStationPointerDown(
|
||||||
|
event,
|
||||||
|
station.id,
|
||||||
|
{
|
||||||
|
x: rotatedX,
|
||||||
|
y: rotatedY,
|
||||||
|
},
|
||||||
|
{ x: offsetX, y: offsetY }
|
||||||
|
)
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "none",
|
position: "absolute",
|
||||||
transformOrigin: "left center",
|
left: cssX,
|
||||||
transform: `rotate(${rotationCss})`,
|
top: cssY,
|
||||||
|
transform: transformCss,
|
||||||
|
color: "#fff",
|
||||||
|
fontFamily: "Roboto, sans-serif",
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
touchAction: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
transformOrigin: "left center",
|
transformOrigin: "left center",
|
||||||
transform: `rotate(${counterRotationCss})`,
|
transform: `rotate(${rotationCss})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
|
||||||
display: "inline-block",
|
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
|
transformOrigin: "left center",
|
||||||
|
transform: `rotate(${counterRotationCss})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 700,
|
position: "relative",
|
||||||
fontSize: primaryFontSize,
|
display: "inline-block",
|
||||||
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{station.name}
|
|
||||||
</div>
|
|
||||||
{showSecondary ? (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
fontWeight: 700,
|
||||||
top: "100%",
|
fontSize: primaryFontSize,
|
||||||
marginTop: -1 * secondaryMarginTop,
|
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: secondaryFontSize,
|
|
||||||
lineHeight: secondaryLineHeight,
|
|
||||||
color: "#CBCBCB",
|
|
||||||
textShadow: "0 0 3px rgba(0,0,0,0.4)",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
...secondaryPositionStyle,
|
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{translatedStation?.name}
|
{station.name}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{showSecondary ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
marginTop: -1 * secondaryMarginTop,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: secondaryFontSize,
|
||||||
|
lineHeight: secondaryLineHeight,
|
||||||
|
color: "#CBCBCB",
|
||||||
|
textShadow: "0 0 3px rgba(0,0,0,0.4)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...secondaryPositionStyle,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translatedStation?.name}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{hoveredStationId === station.id && (
|
||||||
{hoveredStationId === station.id && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
paddingTop: menuPaddingTop,
|
|
||||||
pointerEvents: "auto",
|
|
||||||
zIndex: 10,
|
|
||||||
cursor: "default",
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
position: "absolute",
|
||||||
gap: 4,
|
top: "100%",
|
||||||
padding: 4,
|
left: "50%",
|
||||||
borderRadius: 4,
|
transform: "translateX(-50%)",
|
||||||
backgroundColor: "white",
|
paddingTop: menuPaddingTop,
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
pointerEvents: "auto",
|
||||||
|
zIndex: 10,
|
||||||
|
cursor: "default",
|
||||||
}}
|
}}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{buttons.map((btn) => (
|
<div
|
||||||
<div
|
style={{
|
||||||
key={btn.value}
|
display: "flex",
|
||||||
onClick={(e) => {
|
gap: 4,
|
||||||
e.stopPropagation();
|
padding: 4,
|
||||||
setStationAlignments((prev) => {
|
borderRadius: 4,
|
||||||
const next = new Map(prev);
|
backgroundColor: "white",
|
||||||
next.set(
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||||
station.id,
|
}}
|
||||||
btn.align as StationAlignment
|
>
|
||||||
);
|
{buttons.map((btn) => (
|
||||||
return next;
|
<div
|
||||||
});
|
key={btn.value}
|
||||||
setStationAlign(station.id, btn.value);
|
onClick={(e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
style={{
|
setStationAlignments((prev) => {
|
||||||
padding: "4px 8px",
|
const next = new Map(prev);
|
||||||
fontSize: 12,
|
next.set(
|
||||||
cursor: "pointer",
|
station.id,
|
||||||
backgroundColor:
|
btn.align as StationAlignment
|
||||||
alignment === btn.align
|
);
|
||||||
? "#e0e0e0"
|
return next;
|
||||||
: "transparent",
|
});
|
||||||
borderRadius: 4,
|
setStationAlign(station.id, btn.value);
|
||||||
color: "black",
|
}}
|
||||||
fontWeight: 500,
|
style={{
|
||||||
userSelect: "none",
|
padding: "4px 8px",
|
||||||
}}
|
fontSize: 12,
|
||||||
>
|
cursor: "pointer",
|
||||||
{btn.label}
|
backgroundColor:
|
||||||
</div>
|
alignment === btn.align
|
||||||
))}
|
? "#e0e0e0"
|
||||||
|
: "transparent",
|
||||||
|
borderRadius: 4,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
color: "black",
|
||||||
|
fontWeight: 500,
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{btn.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -283,16 +283,12 @@ class SnapshotStore {
|
|||||||
{ headers: { "X-Request-ID": this.lastRequestId } }
|
{ headers: { "X-Request-ID": this.lastRequestId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Не ждем здесь, так как статус будет загружен в компоненте
|
|
||||||
// this.getSnapshotStatus(response.data.ID);
|
|
||||||
|
|
||||||
return response.data.ID;
|
return response.data.ID;
|
||||||
};
|
};
|
||||||
|
|
||||||
getSnapshotStatus = async (id: string) => {
|
getSnapshotStatus = async (id: string) => {
|
||||||
const response = await authInstance.get(`/snapshots/status/${id}`);
|
const response = await authInstance.get(`/snapshots/status/${id}`);
|
||||||
|
|
||||||
console.log(response.data);
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.snapshotStatus = response.data;
|
this.snapshotStatus = response.data;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user