fix: add anchor for station

This commit is contained in:
2025-11-28 02:36:03 +03:00
parent c5c5f835bc
commit 79539d0583
2 changed files with 170 additions and 171 deletions

View File

@@ -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>
); );
})} })}

View File

@@ -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;
}); });