From 69e7b890a5d7f51a2632a52bcc2e327cdc5c9750 Mon Sep 17 00:00:00 2001 From: Arthri <41360489+Arthri@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:48:23 +0800 Subject: [PATCH] NanoMap QoL Changes (#25487) * Introduce constants for map size and pixes per turf * Scale markers with zoom * Remove unused state * Center map by default * Fix zooming offset * Add view reset button * Fix exaggerated dragging when zoomed * Remove zoom from local state * Rewrite centering code * Allow opening air alarms from map view * Rename Marker to MarkerIcon * Factor out generic map marker * Implement name highlighting * Fix oversight * Save settings across UI opens * Do not sanitize air alarm names They are already automatically sanitized by Inferno * Build and update tgui * force ci * Make labelStyle optional * Make labelStyle optional again * [ci skip] * Build and update /tg/ui * Reformat /tg/ui * Autodec variables [ci skip] --------- Co-authored-by: Arthri <41360489+a@users.noreply.github.com> Co-authored-by: /tg/ui Builder <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> --- .../atmospherics/machinery/airalarm.dm | 10 +- code/modules/tgui/modules/crew_monitor.dm | 54 ++++ tgui/packages/tgui/components/LabeledList.tsx | 9 +- tgui/packages/tgui/components/NanoMap.js | 120 +++++++-- tgui/packages/tgui/interfaces/AtmosControl.js | 9 +- tgui/packages/tgui/interfaces/CrewMonitor.js | 211 +++++++++------ tgui/packages/tgui/styles/atomic/color.scss | 6 + .../tgui/styles/components/NanoMap.scss | 3 +- .../tgui/styles/interfaces/CrewMonitor.scss | 26 ++ tgui/packages/tgui/styles/main.scss | 1 + tgui/public/tgui-panel.bundle.css | 2 +- tgui/public/tgui-panel.bundle.js | 244 +++++++++--------- tgui/public/tgui.bundle.css | 2 +- tgui/public/tgui.bundle.js | 64 ++--- 14 files changed, 490 insertions(+), 271 deletions(-) create mode 100644 tgui/packages/tgui/styles/interfaces/CrewMonitor.scss diff --git a/code/modules/atmospherics/machinery/airalarm.dm b/code/modules/atmospherics/machinery/airalarm.dm index 1381017f1c2..1363484743c 100644 --- a/code/modules/atmospherics/machinery/airalarm.dm +++ b/code/modules/atmospherics/machinery/airalarm.dm @@ -654,7 +654,7 @@ /obj/machinery/alarm/ui_data(mob/user) var/list/data = list() - data["name"] = sanitize(name) + data["name"] = name data["air"] = ui_air_status() data["alarmActivated"] = alarmActivated || danger_level == ATMOS_ALARM_DANGER data["thresholds"] = generate_thresholds_menu() @@ -694,7 +694,7 @@ for(var/obj/machinery/atmospherics/unary/vent_pump/P as anything in alarm_area.vents) var/list/vent_info = list() vent_info["id_tag"] = P.UID() - vent_info["name"] = sanitize(P.name) + vent_info["name"] = P.name vent_info["power"] = P.on vent_info["direction"] = P.releasing vent_info["checks"] = P.pressure_checks @@ -707,7 +707,7 @@ for(var/obj/machinery/atmospherics/unary/vent_scrubber/S as anything in alarm_area.scrubbers) var/list/scrubber_info = list() scrubber_info["id_tag"] = S.UID() - scrubber_info["name"] = sanitize(S.name) + scrubber_info["name"] = S.name scrubber_info["power"] = S.on scrubber_info["scrubbing"] = S.scrubbing scrubber_info["widenet"] = S.widenet @@ -722,11 +722,11 @@ /obj/machinery/alarm/proc/get_console_data(mob/user) var/list/data = list() - data["name"] = sanitize(name) + data["name"] = name data["ref"] = "\ref[src]" data["danger"] = max(danger_level, alarm_area.atmosalm) var/area/A = get_area(src) - data["area"] = sanitize(A.name) + data["area"] = A.name var/turf/T = get_turf(src) data["x"] = T.x data["y"] = T.y diff --git a/code/modules/tgui/modules/crew_monitor.dm b/code/modules/tgui/modules/crew_monitor.dm index 65dc8e2ef86..7b8df4bae23 100644 --- a/code/modules/tgui/modules/crew_monitor.dm +++ b/code/modules/tgui/modules/crew_monitor.dm @@ -1,9 +1,26 @@ +#define MIN_ZOOM 1 +#define MAX_ZOOM 8 +#define MIN_TAB_INDEX 0 +#define MAX_TAB_INDEX 1 + /datum/ui_module/crew_monitor name = "Crew monitor" var/is_advanced = FALSE var/viewing_current_z_level /// If true, we'll see everyone, regardless of their suit sensors. var/ignore_sensors = FALSE + /// The ID of the currently opened UI tab + var/tab_index = 0 + /// The zoom level of the UI map view + var/zoom = 1 + /// The X offset of the UI map + var/offset_x = 0 + /// The Y offset of the UI map + var/offset_y = 0 + /// A list of displayed names. Displayed names were intentionally chosen over ckeys, + /// refs, or uids, because exposing any of the aforementioned to the client could allow + /// an exploit to detect changelings on sensors. + var/highlighted_names = list() /datum/ui_module/crew_monitor/ui_act(action, params) if(..()) @@ -31,6 +48,33 @@ if(!is_advanced) return viewing_current_z_level = text2num(params["new_level"]) + if("set_tab_index") + var/new_tab_index = text2num(params["tab_index"]) + if(isnull(new_tab_index) || new_tab_index < MIN_TAB_INDEX || new_tab_index > MAX_TAB_INDEX) + return + tab_index = new_tab_index + if("set_zoom") + var/new_zoom = text2num(params["zoom"]) + if(isnull(new_zoom) || new_zoom < MIN_ZOOM || new_zoom > MAX_ZOOM) + return + zoom = new_zoom + if("set_offset") + var/new_offset_x = text2num(params["offset_x"]) + var/new_offset_y = text2num(params["offset_y"]) + if(isnull(new_offset_x) || isnull(new_offset_y)) + return + offset_x = new_offset_x + offset_y = new_offset_y + if("add_highlighted_name") + // Intentionally not sanitized as the name is not used for rendering + var/name = params["name"] + highlighted_names += list(name) + if("remove_highlighted_name") + // Intentionally not sanitized as the name is not used for rendering + var/name = params["name"] + highlighted_names -= list(name) + if("clear_highlighted_names") + highlighted_names = list() /datum/ui_module/crew_monitor/ui_state(mob/user) return GLOB.default_state @@ -56,11 +100,16 @@ viewing_current_z_level = level_name_to_num(MAIN_STATION) // by default, set it to the station data["viewing_current_z_level"] = viewing_current_z_level + data["tabIndex"] = tab_index + data["zoom"] = zoom + data["offsetX"] = offset_x + data["offsetY"] = offset_y data["isAI"] = isAI(user) data["isObserver"] = isobserver(user) data["ignoreSensors"] = ignore_sensors data["crewmembers"] = GLOB.crew_repository.health_data(viewing_current_z_level, ignore_sensors) + data["highlightedNames"] = highlighted_names data["critThreshold"] = HEALTH_THRESHOLD_CRIT return data @@ -82,3 +131,8 @@ /datum/ui_module/crew_monitor/ghost/ui_state(mob/user) return GLOB.observer_state + +#undef MIN_ZOOM +#undef MAX_ZOOM +#undef MIN_TAB_INDEX +#undef MAX_TAB_INDEX diff --git a/tgui/packages/tgui/components/LabeledList.tsx b/tgui/packages/tgui/components/LabeledList.tsx index a3e794a9245..d90b1a24667 100644 --- a/tgui/packages/tgui/components/LabeledList.tsx +++ b/tgui/packages/tgui/components/LabeledList.tsx @@ -33,6 +33,7 @@ type LabeledListItemProps = { /** @deprecated */ content?: any; children?: InfernoNode; + labelStyle?: Record; }; const LabeledListItem = (props: LabeledListItemProps) => { @@ -47,10 +48,16 @@ const LabeledListItem = (props: LabeledListItemProps) => { content, children, preserveWhitespace, + labelStyle, } = props; let listItem = ( - + {label ? label + ':' : null} { if (e.stopPropagation) { e.stopPropagation(); @@ -26,13 +30,12 @@ export class NanoMap extends Component { const Ycenter = window.innerHeight / 2 - 256; this.state = { - offsetX: 128, - offsetY: 48, - transform: 'none', + offsetX: props.offsetX ?? 0, + offsetY: props.offsetY ?? 0, dragging: false, originX: null, originY: null, - zoom: 1, + zoom: props.zoom ?? 1, }; // Dragging @@ -54,13 +57,14 @@ export class NanoMap extends Component { const newOffsetX = e.screenX - state.originX; const newOffsetY = e.screenY - state.originY; if (prevState.dragging) { - state.offsetX += newOffsetX; - state.offsetY += newOffsetY; + state.offsetX += newOffsetX / state.zoom; + state.offsetY += newOffsetY / state.zoom; state.originX = e.screenX; state.originY = e.screenY; } else { state.dragging = true; } + props.onOffsetChange?.(e, state); return state; }); pauseEvent(e); @@ -80,16 +84,31 @@ export class NanoMap extends Component { this.handleZoom = (_e, value) => { this.setState((state) => { const newZoom = Math.min(Math.max(value, 1), 8); - let zoomDiff = (newZoom - state.zoom) * 1.5; state.zoom = newZoom; - state.offsetX = state.offsetX - 262 * zoomDiff; - state.offsetY = state.offsetY - 256 * zoomDiff; if (props.onZoom) { props.onZoom(state.zoom); } return state; }); }; + + this.handleReset = (e) => { + this.setState((state) => { + state.offsetX = 0; + state.offsetY = 0; + state.zoom = 1; + this.handleZoom(e, 1); + props.onOffsetChange?.(e, state); + }); + }; + } + + getChildContext() { + return { + map: { + zoom: this.state.zoom, + }, + }; } render() { @@ -98,14 +117,17 @@ export class NanoMap extends Component { const { children } = this.props; const mapUrl = config.map + '_nanomap_z1.png'; - const mapSize = 510 * zoom + 'px'; + const mapSize = MAP_SIZE * zoom + 'px'; const newStyle = { width: mapSize, height: mapSize, - 'margin-top': offsetY + 'px', - 'margin-left': offsetX + 'px', + 'margin-top': offsetY * zoom + 'px', + 'margin-left': offsetX * zoom + 'px', 'overflow': 'hidden', 'position': 'relative', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', 'background-size': 'cover', 'background-repeat': 'no-repeat', 'text-align': 'center', @@ -128,21 +150,35 @@ export class NanoMap extends Component { {children} - + ); } } const NanoMapMarker = (props, context) => { - const { x, y, zoom = 1, icon, tooltip, color } = props; - const rx = x * 2 * zoom - zoom - 3; - const ry = y * 2 * zoom - zoom - 3; + const { + map: { zoom }, + } = context; + const { x, y, icon, tooltip, color, children, ...rest } = props; + const pixelsPerTurfAtZoom = PIXELS_PER_TURF * zoom; + // For some reason the X and Y are offset by 1 + const rx = (x - 1) * pixelsPerTurfAtZoom; + const ry = (y - 1) * pixelsPerTurfAtZoom; return (
- - + + {children}
@@ -151,19 +187,47 @@ const NanoMapMarker = (props, context) => { NanoMap.Marker = NanoMapMarker; +const NanoMapMarkerIcon = (props, context) => { + const { + map: { zoom }, + } = context; + const { icon, color, ...rest } = props; + const markerSize = PIXELS_PER_TURF * zoom + 4 / Math.ceil(zoom / 4); + return ( + + + + ); +}; + +NanoMap.MarkerIcon = NanoMapMarkerIcon; + const NanoMapZoomer = (props, context) => { return ( - - v + 'x'} - value={props.zoom} - onDrag={(e, v) => props.onZoom(e, v)} - /> + + + v + 'x'} + value={props.zoom} + onDrag={(e, v) => props.onZoom(e, v)} + /> +