diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 92a8e2e8d5a..9df55cd5fd7 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -210,7 +210,7 @@ name = "Lighting plate" documentation = "Anything on this plane will be multiplied with the plane it's rendered onto (typically the game plane).\
That's how lighting functions at base. Because it uses BLEND_MULTIPLY and occasionally color matrixes, it needs a backdrop of blackness.\ -
See This byond post\ +
See this byond post\
Lemme see uh, we're masked by the emissive plane so it can actually function (IE: make things glow in the dark).\
We're also masked by the overlay lighting plane, which contains all the well overlay lights in the game. It draws to us and also the game plane.\
Masks us out so it has the breathing room to apply its effect.\ diff --git a/code/modules/admin/verbs/plane_debugger.dm b/code/modules/admin/verbs/plane_debugger.dm index 90a40a6ce08..8c9f1fbfa32 100644 --- a/code/modules/admin/verbs/plane_debugger.dm +++ b/code/modules/admin/verbs/plane_debugger.dm @@ -7,6 +7,9 @@ var/current_group = PLANE_GROUP_MAIN /// Weakref to the mob to edit var/datum/weakref/mob_ref + /// Has the target been set explicitly (via VV) or implicitly (via orbit) + /// Orbit targets will get unset whenever you stop orbiting them + var/explicit_mirror = FALSE var/datum/visual_data/tracking/stored var/datum/visual_data/mirroring/mirror @@ -22,7 +25,7 @@ owner = null return ..() -/datum/plane_master_debug/proc/set_target(mob/new_mob) +/datum/plane_master_debug/proc/set_target(mob/new_mob, explicit = TRUE) QDEL_NULL(mirror) QDEL_NULL(stored) @@ -39,10 +42,13 @@ RegisterSignal(owner.owner.mob, COMSIG_MOB_LOGOUT, PROC_REF(on_our_logout), override = TRUE) mirror = new() mirror.shadow(new_mob) + SStgui.update_uis(owner.owner.mob) if(new_mob == owner.owner.mob) + explicit_mirror = FALSE return + explicit_mirror = explicit create_store() /datum/plane_master_debug/proc/on_our_logout(mob/source) @@ -62,10 +68,16 @@ mirror.set_mirror_target(owner.owner.mob) /datum/plane_master_debug/proc/get_target() - var/mob/target = mob_ref?.resolve() - if(!target?.hud_used) + var/mob/cur_target = mob_ref?.resolve() + var/mob/target = cur_target + if(!target?.hud_used || !explicit_mirror) target = owner.owner.mob - set_target(target) + if (ismob(target.orbit_target)) // If we're orbiting someone, swap to them if possible + var/mob/as_mob = target.orbit_target + if (as_mob.hud_used) + target = target.orbit_target + if (cur_target != target) + set_target(target, FALSE) return target /// Setter for mirror_target, basically allows for enabling/disabiling viewing through mob's sight @@ -73,8 +85,8 @@ if(value == mirror_target) return mirror_target = value - // Refresh our target and mirrors and such - set_target(get_target()) + // Refresh our target and mirrors and such, but keep explicit/implicit mirroring + set_target(get_target(), explicit_mirror) /datum/plane_master_debug/ui_state(mob/user) return ADMIN_STATE(R_DEBUG) @@ -99,12 +111,12 @@ var/datum/hud/our_hud = reference_frame.hud_used var/list/our_groups = our_hud.master_groups - if(!our_groups[current_group]) + if (!our_groups[current_group]) // We assume we'll always have at least one group current_group = our_groups[length(our_hud.master_groups)] var/list/groups = list() - for(var/key in our_groups) + for (var/key in our_groups) groups += key data["enable_group_view"] = length(groups) > 1 @@ -112,30 +124,14 @@ data["present_groups"] = groups var/list/plane_info = list() - data["plane_info"] = plane_info - var/list/relay_deets = list() - data["relay_info"] = relay_deets - var/list/filter_connections = list() - data["filter_connect"] = filter_connections - - var/list/filter_queue = list() - - // Assoc of render targets -> planes - // Gotta be able to look these up so filter stuff can work - var/list/render_target_to_plane = list() - // Assoc list of pending planes -> relays - // Used to ensure the incoming_relays list is filled, even if the relay's generated before the plane's processed - var/list/pending_relays = list() var/list/our_planes = our_hud?.get_planes_from(current_group) - for(var/plane_string as anything in our_planes) + for (var/plane_string as anything in our_planes) var/list/this_plane = list() var/atom/movable/screen/plane_master/plane = our_planes[plane_string] - var/string_plane = "[plane.plane]" this_plane["name"] = plane.name this_plane["documentation"] = plane.documentation this_plane["plane"] = plane.plane - this_plane["our_ref"] = string_plane this_plane["offset"] = plane.offset this_plane["real_plane"] = plane.real_plane this_plane["renders_onto"] = plane.render_relay_planes @@ -143,191 +139,39 @@ this_plane["color"] = plane.color this_plane["alpha"] = plane.alpha this_plane["render_target"] = plane.render_target - this_plane["intended_hidden"] = plane.force_hidden + this_plane["force_hidden"] = plane.force_hidden + var/list/relays = list() + var/list/filters = list() - var/list/incoming_relays = list() - this_plane["incoming_relays"] = incoming_relays - - for(var/pending_relay in pending_relays[string_plane]) - incoming_relays += pending_relay - var/list/this_relay = relay_deets[pending_relay] - this_relay["target_index"] = length(incoming_relays) - - - this_plane["outgoing_relays"] = list() - - // You can think of relays as connections between plane master "nodes - // They do have some info of their own tho, best to pass that along - for(var/atom/movable/render_plane_relay/relay in plane.relays) - var/string_target = "[relay.plane]" + for (var/atom/movable/render_plane_relay/relay as anything in plane.relays) var/list/this_relay = list() this_relay["name"] = relay.name this_relay["source"] = plane.plane - this_relay["source_ref"] = string_plane this_relay["target"] = relay.plane - this_relay["target_ref"] = string_target this_relay["layer"] = relay.layer + this_relay["our_ref"] = "[plane.plane]-[relay.plane]" + this_relay["blend_mode"] = GLOB.blend_names["[relay.blend_mode]"] + relays += list(this_relay) - // Now taht we've encoded our relay, we need to hand out references to it to our source plane, alongside the target plane - var/relay_ref = "[string_plane]-[string_target]" - this_relay["our_ref"] = relay_ref - relay_deets[relay_ref] = this_relay - this_plane["outgoing_relays"] += relay_ref - - // If we've already encoded our target plane, update its incoming relays list - // Otherwise, we'll handle this later - var/list/existing_target = plane_info[string_target] - if(existing_target) - existing_target["incoming_relays"] += relay_ref - else - var/list/pending_plane = pending_relays[string_target] - if(!pending_plane) - pending_plane = list() - pending_relays[string_target] = pending_plane - pending_plane += relay_ref - - this_plane["incoming_filters"] = list() - this_plane["outgoing_filters"] = list() - // We're gonna collect a list of filters, partly because they're useful info - // But also because they can be used as connections, and we need to support that - for(var/filter_id in plane.filter_data) + for (var/filter_id in plane.filter_data) var/list/filter = plane.filter_data[filter_id] if(!filter["render_source"]) continue + var/list/filter_info = filter.Copy() - filter_info["target_ref"] = string_plane filter_info["name"] = filter_id - filter_queue += list(filter_info) + filter_info["our_ref"] = "[plane.plane]-[filter_id]" + filters += list(filter_info) - plane_info[plane_string] = this_plane - render_target_to_plane[plane.render_target] = this_plane + this_plane["relays"] = relays + this_plane["filters"] = filters - for(var/list/filter in filter_queue) - var/source = filter["render_source"] - var/list/source_plane = render_target_to_plane[source] - var/list/target_plane = plane_info[filter["target_ref"]] - var/source_ref = source_plane["our_ref"] - filter["source_ref"] = source_ref - var/our_ref = "[source_ref]-[filter["target_ref"]]-filter" - filter["our_ref"] = our_ref - filter_connections[our_ref] = filter - source_plane["outgoing_filters"] += our_ref - target_plane["incoming_filters"] += our_ref + plane_info += list(this_plane) - // Only load this once. Prevents leaving off orphaned components - if(!depth_stack[current_group]) - depth_stack[current_group] = treeify(plane_info, relay_deets, filter_connections) - - // We will use this js side to arrange our plane masters and such - // It's essentially a stack of where they should be displayed - data["depth_stack"] = depth_stack[current_group] + data["planes"] += plane_info return data -// Reading this in the queue tells the search to increase the depth, and then push another increase command to the end of the stack -// This way we ensure groupings always stay together, and depth is respected -#define COMMAND_DEPTH_INCREASE "increase_depth" -#define COMMAND_NEXT_PARENT "next_parent" - -/// Takes a list of js formatted planes, and turns it into a tree based off the back connections of relays -/// So start at the top master plane, and work down -/// Haha jerry what if I added commands to my list parser lmao lol -/datum/plane_master_debug/proc/treeify(list/plane_info, list/relay_info, list/filter_connections) - // List in the form [depth in num] -> list(list(plane_ref -> parent_ref, ...), ...) - var/list/treelike_output = list() - // List in the form plane ref -> current depth - var/list/plane_to_depth = list() - // List of items/commands to process. FIFO queue, to ensure the brackets are built correctly - var/list/processing_queue = list() - // A FIFO queue of parents. Used so planes can have refs to their direct parent, to make sorting easier - var/list/parents = list("") - var/parent_head = 1 - // The current depth of our search, used with treelike_output - var/depth = 0 - // Push a depth increase onto the queue, to properly setup the sorta looping effect it has - processing_queue += COMMAND_DEPTH_INCREASE - processing_queue += "[RENDER_PLANE_MASTER]" - // We need to do a c style loop here because we are expanding the queue, and so need to update our conditional - for(var/i = 1; i <= length(processing_queue); i++) - var/entry = processing_queue[i] - // We've reached the end of a depth block - // Increment the depth and stick another command on the end of the queue - if(entry == COMMAND_DEPTH_INCREASE) - // The plane to continue on with, assuming we can find an unvisited head to use - var/continue_on_with = "" - // Don't wanna infinite loop now - if(i == length(processing_queue)) - for(var/plane in TRUE_PLANE_TO_OFFSETS(RENDER_PLANE_MASTER)) - if(!plane_to_depth["[plane]"]) - continue_on_with = "[plane]" - // We only want to handle one plane master at a time - break - if(!continue_on_with) - continue - // Increment our depth - depth += 1 - treelike_output += list(list()) - // If this isn't the end, stick another entry on the end to ensure batches work proper - processing_queue += COMMAND_DEPTH_INCREASE - // If we found a plane to use to extend our process, tack it on the end here as god intended - if(continue_on_with) - processing_queue += continue_on_with - continue - if(entry == COMMAND_NEXT_PARENT) - parent_head += 1 - continue - - var/old_queue_len = length(processing_queue) - var/existing_depth = plane_to_depth[entry] - // If we've seen you before, remove your last entry - // We always want inputs before outputs in the stack - if(existing_depth) - treelike_output[existing_depth] -= entry - - // If it's not a command, it must be a plane string - var/list/plane = plane_info[entry] - /// We want master planes to ALWAYS bubble down to their own space. - /// Just ignore this if this is the head we're processing, yeah? - if(PLANE_TO_TRUE(plane["real_plane"]) == RENDER_PLANE_MASTER && i > 2) - // If there's other stuff already in your depth entry, or there's more then one thing (a depth increase command) - // Left in the queue, "bubble" down a layer. - if(length(treelike_output[depth]) || i + 1 != length(processing_queue)) - processing_queue += COMMAND_NEXT_PARENT - parents += parents[parent_head] - processing_queue += entry - continue - // Add all the planes that pipe into us to the queue, Intentionally allows dupes - // If we find the same entry twice, it'll get moved down the depth stack - for(var/relay_string in plane["incoming_relays"]) - var/list/relay = relay_info[relay_string] - processing_queue += relay["source_ref"] - for(var/filter_ref in plane["incoming_filters"]) - var/list/filter = filter_connections[filter_ref] - processing_queue += filter["source_ref"] - - // If the queue has grown, we're a parent, so stick us in the parent queue - if(old_queue_len != length(processing_queue)) - parents += entry - // Stick a parent increase right before our children show up in the queue. That way we're properly set as their parent - processing_queue.Insert(old_queue_len + 1, COMMAND_NEXT_PARENT) - // Stick us in the output at our designated depth - var/list/plane_packet = list() - plane_packet[entry] = parents[parent_head] - treelike_output[depth] += plane_packet - plane_to_depth[entry] = depth - - /// Walk treelike output, remove allll the empty lists we've accidentially generated - for(var/depth_index = 1; depth_index <= length(treelike_output); depth_index++) - var/list/layer = treelike_output[depth_index] - if(!length(layer)) - treelike_output.Cut(depth_index, depth_index + 1) - depth_index -= 1 - - return treelike_output - -#undef COMMAND_DEPTH_INCREASE -#undef COMMAND_NEXT_PARENT - /datum/plane_master_debug/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() if(.) @@ -343,51 +187,63 @@ switch(action) if("rebuild") group.rebuild_hud() + if("reset_mob") set_target(null) + if("toggle_mirroring") set_mirroring(!mirror_target) + if("vv_mob") owner.owner.debug_variables(reference_frame) + if("set_group") current_group = params["target_group"] + if("connect_relay") var/source_plane = params["source"] var/target_plane = params["target"] + var/blend_mode = text2num(params["mode"]) var/atom/movable/screen/plane_master/source = our_planes["[source_plane]"] if(source.get_relay_to(target_plane)) // Fuck off return - source.add_relay_to(target_plane) + source.add_relay_to(target_plane, blend_mode != BLEND_DEFAULT ? blend_mode : null) return TRUE + if("disconnect_relay") var/source_plane = params["source"] var/target_plane = params["target"] var/atom/movable/screen/plane_master/source = our_planes["[source_plane]"] source.remove_relay_from(text2num(target_plane)) return TRUE + if("disconnect_filter") var/target_plane = params["target"] var/atom/movable/screen/plane_master/filtered_plane = our_planes["[target_plane]"] filtered_plane.remove_filter(params["name"]) return TRUE + if("vv_plane") var/plane_edit = params["edit"] var/atom/movable/screen/plane_master/edit = our_planes["[plane_edit]"] var/mob/user = ui.user user?.client?.debug_variables(edit) return TRUE + if("set_alpha") var/plane_edit = params["edit"] var/atom/movable/screen/plane_master/edit = our_planes["[plane_edit]"] var/newalpha = params["alpha"] animate(edit, 0.4 SECONDS, alpha = newalpha) return TRUE + if("edit_color_matrix") var/plane_edit = params["edit"] var/atom/movable/screen/plane_master/edit = our_planes["[plane_edit]"] var/mob/user = ui.user user?.client?.open_color_matrix_editor(edit) return TRUE + if("edit_filters") var/plane_edit = params["edit"] var/atom/movable/screen/plane_master/edit = our_planes["[plane_edit]"] diff --git a/tgui/packages/tgui/drag.ts b/tgui/packages/tgui/drag.ts index 131b5d3e4e6..09eacfe1f80 100644 --- a/tgui/packages/tgui/drag.ts +++ b/tgui/packages/tgui/drag.ts @@ -40,7 +40,7 @@ export const getWindowSize = (): [number, number] => [ ]; // Set window position -const setWindowPosition = (vec: [number, number]) => { +export const setWindowPosition = (vec: [number, number]) => { const byondPos = vecAdd(vec, screenOffset); return Byond.winset(Byond.windowId, { pos: byondPos[0] + ',' + byondPos[1], diff --git a/tgui/packages/tgui/interfaces/AlertModal.tsx b/tgui/packages/tgui/interfaces/AlertModal.tsx index a3f41b7d85a..cdac53e6f79 100644 --- a/tgui/packages/tgui/interfaces/AlertModal.tsx +++ b/tgui/packages/tgui/interfaces/AlertModal.tsx @@ -36,7 +36,7 @@ export function AlertModal(props) { // Stolen wholesale from fontcode function textWidth(text: string, font: string, fontsize: number) { // default font height is 12 in tgui - font = fontsize + 'x ' + font; + font = fontsize + 'px ' + font; const c = document.createElement('canvas'); const ctx = c.getContext('2d') as CanvasRenderingContext2D; ctx.font = font; @@ -53,7 +53,7 @@ export function AlertModal(props) { // At least one of the buttons has a long text message const isVerbose = buttons.some( (button) => - textWidth(button, '', large_buttons ? 14 : 12) > // 14 is the larger font size for large buttons + textWidth(button, 'Verdana, Geneva', large_buttons ? 14 : 12) > // 14 is the larger font size for large buttons windowWidth / buttons.length - paddingMagicNumber, ); const largeSpacing = isVerbose && large_buttons ? 20 : 15; diff --git a/tgui/packages/tgui/interfaces/MCDependencyDebug.tsx b/tgui/packages/tgui/interfaces/MCDependencyDebug.tsx index 52c4b20fc07..8ef76f461ca 100644 --- a/tgui/packages/tgui/interfaces/MCDependencyDebug.tsx +++ b/tgui/packages/tgui/interfaces/MCDependencyDebug.tsx @@ -79,7 +79,6 @@ function GraphNode(props: GraphNodeProps) { style={{ width: '100%', height: '100%', - position: 'absolute', }} viewBox="0, 0, 100, 100" > @@ -110,7 +109,6 @@ function GraphNode(props: GraphNodeProps) { style={{ width: '100%', height: '100%', - position: 'absolute', }} viewBox="0, 0, 100, 100" > @@ -213,7 +211,9 @@ export function MCDependencyDebug(props) { const subsystemLayer: SubsystemLayer = {}; for (let i = 0; i < subsystemsGraph.length; i++) { const subsystem = subsystemsGraph[i]; - evaluateSubsystemLayer(subsystem, 1, subsystemLayer); + if (subsystem.dependents.length === 0) { + evaluateSubsystemLayer(subsystem, 1, subsystemLayer); + } } return subsystemLayer; }, [subsystems]); diff --git a/tgui/packages/tgui/interfaces/PlaneMasterDebug.tsx b/tgui/packages/tgui/interfaces/PlaneMasterDebug.tsx deleted file mode 100644 index cf17c90e565..00000000000 --- a/tgui/packages/tgui/interfaces/PlaneMasterDebug.tsx +++ /dev/null @@ -1,1116 +0,0 @@ -import { sortBy } from 'common/collections'; -import { Component, createRef, RefObject } from 'react'; -import { - Box, - Button, - Dropdown, - InfinitePlane, - LabeledList, - Modal, - Section, - Slider, - Stack, - Tooltip, -} from 'tgui-core/components'; -import { flow } from 'tgui-core/fp'; -import { classes, shallowDiffers } from 'tgui-core/react'; - -import { resolveAsset } from '../assets'; -import { useBackend, useLocalState } from '../backend'; -import { Window } from '../layouts'; -import { Connection, Connections, Position } from './common/Connections'; -import { MOUSE_BUTTON_LEFT, noop } from './IntegratedCircuit/constants'; - -enum ConnectionType { - Relay, - Filter, -} - -enum ConnectionDirection { - Incoming, - Outgoing, -} - -type ConnectionRef = { - ref: string; - sort_by: number; -}; - -type Plane = { - name: string; - documentation: string; - plane: number; - our_ref: string; - offset: number; - real_plane: number; - renders_onto: number[]; - blend_mode: number; - color: string | number[]; - alpha: number; - render_target: string; - incoming_relays: string[]; - outgoing_relays: string[]; - incoming_filters: string[]; - outgoing_filters: string[]; - intended_hidden: boolean; - - incoming_connections: ConnectionRef[]; - outgoing_connections: ConnectionRef[]; - - x: number; - y: number; - step_size: number; - size_x: number; - size_y: number; -}; - -type Relay = { - name: string; - layer: number; -}; - -type Filter = { - type: string; - name: string; - render_source: string; -}; - -// Type of something that spawn a connection -type Connected = { - connect_color: string; - source: number; - source_ref: string; - target: number; - target_ref: string; - our_ref: string; - - source_index: number; - target_index: number; - - connect_type: ConnectionType; -}; - -interface AssocPlane { - [index: string]: Plane; -} - -interface AssocRelays { - [index: string]: Relay & Connected; -} - -interface AssocFilters { - [index: string]: Filter & Connected; -} - -interface AssocConnected { - [index: string]: Connected; -} - -interface AssocString { - [index: string]: string; -} - -type PlaneDebugData = { - our_group: string; - present_groups: string[]; - enable_group_view: boolean; - relay_info: AssocRelays; - plane_info: AssocPlane; - filter_connect: AssocFilters; - depth_stack: AssocString[]; - mob_name: string; - mob_ref: string; - our_ref: string; - tracking_active: boolean; -}; - -// Stolen wholesale from fontcode -function textWidth(text: string, font: string, fontsize: number) { - // default font height is 12 in tgui - font = fontsize + 'x ' + font; - const c = document.createElement('canvas'); - const ctx = c.getContext('2d') as CanvasRenderingContext2D; - ctx.font = font; - return ctx.measureText(text).width; -} - -const planeToPosition = function (plane: Plane, index, is_incoming): Position { - return { - x: is_incoming ? plane.x : plane.x + plane.size_x, - y: - 29 + - plane.y + - plane.step_size * index + - (plane.step_size - plane.step_size / 3), - }; -}; - -// Takes a plane, returns the amount of node space it will need -const getPlaneNodeHeight = function (plane: Plane): number { - return Math.max( - plane.incoming_relays.length + plane.incoming_filters.length, - plane.outgoing_relays.length + plane.outgoing_filters.length, - ); -}; - -const sortConnectionRefs = function ( - refs: ConnectionRef[], - direction: ConnectionDirection, - connectSources: AssocConnected, -) { - refs = sortBy(refs, (connection: ConnectionRef) => connection.sort_by); - refs.map((connection, index) => { - let connectSource = connectSources[connection.ref]; - if (direction === ConnectionDirection.Outgoing) { - connectSource.source_index = index; - } else if (direction === ConnectionDirection.Incoming) { - connectSource.target_index = index; - } - }); - return refs; -}; - -const addConnectionRefs = function ( - read_from: string[], - add_type: ConnectionDirection, - add_to: ConnectionRef[], - reference: AssocConnected, - plane_info: AssocPlane, -) { - for (const ref of read_from) { - const connected = reference[ref]; - let our_plane; - // If we're incoming, use the target ref, and vis versa - if (add_type === ConnectionDirection.Incoming) { - our_plane = plane_info[connected.source_ref]; - } else if (add_type === ConnectionDirection.Outgoing) { - our_plane = plane_info[connected.target_ref]; - } - add_to.push({ - ref: ref, - sort_by: our_plane.plane, - }); - } -}; - -// Takes a list of planes, uses the depth stack to position them -const positionPlanes = (connectSources: AssocConnected) => { - const { data } = useBackend(); - const { plane_info, relay_info, filter_connect, depth_stack } = data; - - // First, we concatinate our connection sources - // We need them in one list partly for later purposes - // But also so we can set their source/target index nicely - for (const ref of Object.keys(relay_info)) { - let connection_source: Connected = relay_info[ref]; - connection_source.connect_type = ConnectionType.Relay; - connection_source.connect_color = 'blue'; - connectSources[ref] = connection_source; - } - for (const ref of Object.keys(filter_connect)) { - let connection_source: Connected = filter_connect[ref]; - connection_source.connect_type = ConnectionType.Filter; - connection_source.connect_color = 'purple'; - connectSources[ref] = connection_source; - } - - for (const plane_ref of Object.keys(plane_info)) { - let our_plane = plane_info[plane_ref]; - const incoming_conct: ConnectionRef[] = [] as any; - const outgoing_conct: ConnectionRef[] = [] as any; - addConnectionRefs( - our_plane.incoming_relays, - ConnectionDirection.Incoming, - incoming_conct, - relay_info, - plane_info, - ); - addConnectionRefs( - our_plane.incoming_filters, - ConnectionDirection.Incoming, - incoming_conct, - filter_connect, - plane_info, - ); - addConnectionRefs( - our_plane.outgoing_relays, - ConnectionDirection.Outgoing, - outgoing_conct, - relay_info, - plane_info, - ); - addConnectionRefs( - our_plane.outgoing_filters, - ConnectionDirection.Outgoing, - outgoing_conct, - filter_connect, - plane_info, - ); - our_plane.incoming_connections = sortConnectionRefs( - incoming_conct, - ConnectionDirection.Incoming, - connectSources, - ); - our_plane.outgoing_connections = sortConnectionRefs( - outgoing_conct, - ConnectionDirection.Outgoing, - connectSources, - ); - } - - // First we sort by the plane of each member, - // then we sort by the plane of each member's head - // This way we get a nicely sorted list - // and get rid of the now unneeded parent refs - const stack = depth_stack.map((layer) => - flow([ - (planes) => sortBy(planes, (plane: string) => plane_info[plane].plane), - (planes) => - sortBy(planes, (plane: string) => { - const read_from = plane_info[layer[plane]]; - if (!read_from) { - return 0; - } - return read_from.plane; - }), - ])(Object.keys(layer)), - ); - - let base_x = 0; - let longest_name = 0; - let tallest_stack = 0; - for (const layer of stack) { - base_x += longest_name; - base_x += 150; - let new_longest = 0; - let last_node_len = 0; - let base_y = 0; - for (const plane_ref of layer) { - const old_y = base_y; - const plane = plane_info[plane_ref]; - // - because we want to work backwards rather then forwards - plane.x = -base_x; - // I am assuming the height of a plane master with two connections looks - // like 50% name 50% (two) nodes - base_y += 45; - // One extra for the relay add button - base_y += 19 * (last_node_len + 1); - // We need to know how large node steps are for later - plane.step_size = 19; - plane.y = base_y; - const width = textWidth(plane.name, '', 16) + 30; - plane.size_x = width; - plane.size_y = old_y - base_y; - new_longest = Math.max(new_longest, width); - last_node_len = getPlaneNodeHeight(plane); - } - longest_name = new_longest; - tallest_stack = Math.max(tallest_stack, base_y); - } - - // Now that we've got everything stacked, we need to center it - for (const layer of stack) { - const last_ref = layer[layer.length - 1]; - const last_plane = plane_info[last_ref]; - const delta_tall = tallest_stack - last_plane.y; - // Now that we know how "off" our height is, we can correct it - // We halve because otherwise this looks dumb - const offset = delta_tall / 2; - for (const plane_ref of layer) { - const plane = plane_info[plane_ref]; - plane.y += offset; - } - } -}; - -const arrayRemove = function (arr: any, value) { - return arr.filter((element) => element !== value); -}; - -export class PlaneMasterDebug extends Component { - constructor(props) { - super(props); - this.handlePortClick = this.handlePortClick.bind(this); - } - - handlePortClick(connection: Connected, isOutput, event) { - if (event.button !== MOUSE_BUTTON_LEFT) { - return; - } - const { act, data } = useBackend(); - const { plane_info } = data; - - event.preventDefault(); - if (connection.connect_type === ConnectionType.Relay) { - // Close the connection - act('disconnect_relay', { - source: connection.source_ref, - target: connection.target_ref, - }); - let source_plane = plane_info[connection.source_ref]; - let target_plane = plane_info[connection.source_ref]; - source_plane.outgoing_relays = arrayRemove( - source_plane.outgoing_relays, - connection.our_ref, - ); - target_plane.incoming_relays = arrayRemove( - target_plane.incoming_relays, - connection.our_ref, - ); - } else if (connection.connect_type === ConnectionType.Filter) { - // Close the connection - const filter = connection as Filter & Connected; - act('disconnect_filter', { - target: filter.target_ref, - name: filter.name, - }); - let source_plane = plane_info[connection.source_ref]; - let target_plane = plane_info[connection.source_ref]; - source_plane.outgoing_filters = arrayRemove( - source_plane.outgoing_filters, - connection.our_ref, - ); - target_plane.incoming_filters = arrayRemove( - target_plane.incoming_filters, - connection.our_ref, - ); - } - } - - render() { - const { act, data } = useBackend(); - const { plane_info, mob_name } = data; - const [showAdd, setShowAdd] = useLocalState('showAdd', false); - - const [connectSources, setConnectSouces] = useLocalState( - 'connectionSources', - {}, - ); - - positionPlanes(connectSources); - - const connections: Connection[] = []; - - for (const ref of Object.keys(connectSources)) { - const connect = connectSources[ref]; - const source_plane = plane_info[connect.source_ref]; - const target_plane = plane_info[connect.target_ref]; - connections.push({ - color: connect.connect_color, - from: planeToPosition(source_plane, connect.source_index, false), - to: planeToPosition(target_plane, connect.target_index, true), - ref: ref, - }); - } - - return ( - - - - {Object.keys(plane_info).map( - (plane_key, index) => - plane_key && ( - - ), - )} - - - - - - ); - } -} - -type PlaneMasterProps = { - name: string; - incoming_connections: ConnectionRef[]; - outgoing_connections: ConnectionRef[]; - connected_list: AssocConnected; - our_plane: Plane; - x: number; - y: number; - onPortMouseDown: Function; - act: Function; -}; - -class PlaneMaster extends Component { - shouldComponentUpdate(nextProps, nextState) { - const { incoming_connections, outgoing_connections } = this - .props as PlaneMasterProps; - - return ( - shallowDiffers(this.props, nextProps) || - shallowDiffers(this.state as object, nextState) || - shallowDiffers(incoming_connections, nextProps.incoming_connections) || - shallowDiffers(outgoing_connections, nextProps.outgoing_connections) - ); - } - - render() { - const { - name, - incoming_connections, - outgoing_connections, - connected_list, - our_plane, - x, - y, - onPortMouseDown = noop, - act = noop, - ...rest - } = this.props as PlaneMasterProps; - const [showAdd, setShowAdd] = useLocalState('showAdd', false); - const [currentPlane, setCurrentPlane] = useLocalState('currentPlane', {}); - const [readPlane, setReadPlane] = useLocalState('readPlane', ''); - - // Assigned onto the ports - const PortOptions = { - onPortMouseDown: onPortMouseDown, - }; - return ( - - - {name} - - - - - Alpha ({workingPlane.alpha}) - - - - ); -}; - -const InfoButton = (props) => { - const [showInfo, setShowInfo] = useLocalState('showInfo', false); - const { no_position } = props; - const foreign = has_foreign_mob(); - - return ( - - - - - - - - - ); -}; - -const InfoModal = (props) => { - const [showInfo, setShowInfo] = useLocalState('showInfo', false); - const pain = ''; - const display = { - __html: pain, - }; - return ( - -
setShowInfo(false)} - /> - } - > - -

What is all this?

- This UI exists to help visualize plane masters, the backbone of our - rendering system.
- It also provices some tools for editing and messing with them.
-
-

How to use this UI


- This UI exists primarially as a visualizer, mostly because this info is - quite obscure, and I want it to be easier to understand. -
-
- That said, it also supports editing plane masters, adding and removing - relays, and provides easy access to color matrix/filter/alpha/vv - editing.
-
- To start off with, each little circle represents a{' '} - render_target based connection. -
- Blue nodes are relays, so drawing one plane onto another. Purple ones - are filter based connections.
- You can tell where a node starts and ends based on the side of the plane - it's on.
-
- Adding a new relay is simple, you just need to hit the + button, and - select a plane by name to relay onto.
-
- Each plane can be viewed more closely by clicking the little button in - it's top right corner. This opens a sidebar, and displays a lot of - more general info about the plane and its purpose, alongside exposing - some useful buttons and interesting values.
-
- Planes are aligned based off their initial setup. If you end up breaking - things byond repair, or just want to reset things, you can hit the - recycle button in the top left to totally refresh your plane masters.{' '} -
-
-

What is a plane master?

- You can think of a plane master as a way to group a set of objects onto - one rendering slate.
- It is per client too, which makes it quite powerful. This is done using - the plane variable of /atom.
-
- We first create an atom with an appearance flag that contains{' '} - PLANE_MASTER and give it a plane value.
- Then we mirror the same plane value on all the atoms we - want to render in this group. -
-
- Finally, we place the PLANE_MASTER'd atom in the - relevent client's screen contents.
- That sets up the bare minimum. -
-
- It is worth noting that the plane var does not only effect - this rendering grouping behavior.
- It also effects the layering of objects on the map.
-
- For this reason, there are some effects that are pretty much impossible - with planes.
- Masking one thing while also drawing that thing in the correct order - with other objects on the map is a good example of this. -
- It is possible to do, but it's quite disruptive. -
-
- Normally, planes will just group, apply an effect, and then draw - directly to the game. -
- What if we wanted to draw planes onto other planes then?
-
-

Render Targets and Relays

-
- Rendering one thing onto another is actually not that complex.
- We can set the render_target variable of an atom to relay - it to some render_source.
-
- If that render_target is preceeded by *, it will - not be drawn to the actual client view, and instead just relayed.{' '} -
-
- Ok so we can relay a plane master onto some other atom, but how do we - get it on another plane master? We can't just draw it with{' '} - render_source, since we might want to relay more then one - plane master. -
-
- Why not relay it to another atom then? and then well, set that - atom's plane var to the plane master we want?
-
- That ends up being about what we do.
- It's worth noting that render sources are often used by filters, - normally to apply some displacement or mask. -
-
-

Applying effects


- Ok so we can group and relay planes, but what can we actually do with - that?
-
- Lots of stuff it turns out. Filters are quite powerful, and we use them - quite a bit.
- You can use filters to mask one plane with another, or use one plane as - a distortion source for another.
-
- Can do more basic stuff too, setting a plane's color matrix can be - quite powerful.
- Even just setting alpha to show and hide things can be quite useful.{' '} -
-
- I won't get into every effect we do here, you can learn more about - each plane by clicking on the little button in their top right.
-
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneEditor.tsx b/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneEditor.tsx new file mode 100644 index 00000000000..7946824676c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneEditor.tsx @@ -0,0 +1,145 @@ +import { + Box, + Button, + LabeledList, + Section, + Slider, + Tooltip, +} from 'tgui-core/components'; + +import { Plane } from './types'; +import { usePlaneDebugContext } from './usePlaneDebug'; + +export function PlaneEditor() { + const { activePlane, planesProcessed, setPlaneOpen, act } = + usePlaneDebugContext(); + + const currentPlane: Plane = planesProcessed[activePlane as number]; + const doc_html = { + __html: currentPlane.documentation, + }; + + return ( +
setPlaneOpen(false)} + /> + } + > +
+ +
+ + + + + {currentPlane.plane} + + + + + {currentPlane.offset} + + + + + {currentPlane.render_target + ? `"${currentPlane.render_target}"` + : 'None'} + + + + + {currentPlane.blend_mode} + + + + + {currentPlane.force_hidden ? 'True' : 'False'} + + + +
+
+ + + + + act('set_alpha', { edit: currentPlane.plane, alpha: value }) + } + onChange={(_event, value) => + act('set_alpha', { edit: currentPlane.plane, alpha: value }) + } + > + Alpha ({currentPlane.alpha}) + +
+
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneMaster.tsx b/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneMaster.tsx new file mode 100644 index 00000000000..e6ee8cead35 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PlaneMasterDebug/PlaneMaster.tsx @@ -0,0 +1,162 @@ +import { Box, Button, Stack } from 'tgui-core/components'; +import { classes } from 'tgui-core/react'; + +import { getWindowPosition, setWindowPosition } from '../../drag'; +import { Port } from './Port'; +import { Filter, Plane, PlaneConnectorsMap, Relay } from './types'; +import { usePlaneDebugContext } from './usePlaneDebug'; + +export type PlaneMasterProps = { + plane: Plane; + connectionData: PlaneConnectorsMap; +}; + +export function PlaneMaster(props: PlaneMasterProps) { + const { plane, connectionData } = props; + const { + connectionHighlight, + activePlane, + setActivePlane, + setConnectionOpen, + planeOpen, + setPlaneOpen, + } = usePlaneDebugContext(); + + const incoming_connections: (Filter | Relay)[] = ( + plane.incoming_filters as (Filter | Relay)[] + ) + .concat(plane.incoming_relays) + .filter((x: Filter | Relay) => { + return x.source !== undefined; + }) + .sort( + (a: Filter | Relay, b: Filter | Relay) => + (a.source as Plane).plane - (b.source as Plane).plane, + ); + + const outgoing_connections: (Filter | Relay)[] = ( + plane.outgoing_filters as (Filter | Relay)[] + ) + .concat(plane.outgoing_relays) + .filter((x: Filter | Relay) => { + return x.target !== undefined; + }) + .sort( + (a: Filter | Relay, b: Filter | Relay) => + (a.target as Plane).plane - (b.target as Plane).plane, + ); + + return ( + + + + {plane.name} + + + + + + + ); +} + +function InfoModal() { + const { setInfoOpen } = usePlaneDebugContext(); + return ( + +
setInfoOpen(false)} + /> + } + > +

What is all this?

+ This UI exists to help visualize plane masters, the backbone of our + rendering system.
+ It also provices some tools for editing and messing with them.
+
+

How to use this UI


+ This UI exists primarially as a visualizer, mostly because this info is + quite obscure, and I want it to be easier to understand. +
+
+ That said, it also supports editing plane masters, adding and removing + relays, and provides easy access to color matrix/filter/alpha/vv + editing.
+
+ To start off with, each little circle represents a{' '} + render_target based connection. +
+ Blue nodes are relays, so drawing one plane onto another. Purple ones + are filter based connections.
+ You can tell where a node starts and ends based on the side of the plane + it's on.
+
+ Adding a new relay is simple, you just need to hit the + button, and + select a plane by name to relay onto.
+
+ Each plane can be viewed more closely by clicking the little button in + it's top right corner. This opens a sidebar, and displays a lot of + more general info about the plane and its purpose, alongside exposing + some useful buttons and interesting values.
+
+ Planes are aligned based off their initial setup. If you end up breaking + things byond repair, or just want to reset things, you can hit the + recycle button in the top left to totally refresh your plane masters.{' '} +
+
+

What is a plane master?

+ You can think of a plane master as a way to group a set of objects onto + one rendering slate.
+ It is per client too, which makes it quite powerful. This is done using + the plane variable of /atom.
+
+ We first create an atom with an appearance flag that contains{' '} + PLANE_MASTER and give it a plane value.
+ Then we mirror the same plane value on all the atoms we + want to render in this group. +
+
+ Finally, we place the PLANE_MASTER'd atom in the + relevent client's screen contents.
+ That sets up the bare minimum. +
+
+ It is worth noting that the plane var does not only effect + this rendering grouping behavior.
+ It also effects the layering of objects on the map.
+
+ For this reason, there are some effects that are pretty much impossible + with planes.
+ Masking one thing while also drawing that thing in the correct order + with other objects on the map is a good example of this. +
+ It is possible to do, but it's quite disruptive. +
+
+ Normally, planes will just group, apply an effect, and then draw + directly to the game. +
+ What if we wanted to draw planes onto other planes then?
+
+

Render Targets and Relays

+
+ Rendering one thing onto another is actually not that complex.
+ We can set the render_target variable of an atom to relay + it to some render_source.
+
+ If that render_target is preceeded by *, it will + not be drawn to the actual client view, and instead just relayed.{' '} +
+
+ Ok so we can relay a plane master onto some other atom, but how do we + get it on another plane master? We can't just draw it with{' '} + render_source, since we might want to relay more then one + plane master. +
+
+ Why not relay it to another atom then? and then well, set that + atom's plane var to the plane master we want?
+
+ That ends up being about what we do.
+ It's worth noting that render sources are often used by filters, + normally to apply some displacement or mask. +
+
+

Applying effects


+ Ok so we can group and relay planes, but what can we actually do with + that?
+
+ Lots of stuff it turns out. Filters are quite powerful, and we use them + quite a bit.
+ You can use filters to mask one plane with another, or use one plane as + a distortion source for another.
+
+ Can do more basic stuff too, setting a plane's color matrix can be + quite powerful.
+ Even just setting alpha to show and hide things can be quite useful.{' '} +
+
+ I won't get into every effect we do here, you can learn more about + each plane by clicking on the little button in their top right.
+
+
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/PlaneMasterDebug/Port.tsx b/tgui/packages/tgui/interfaces/PlaneMasterDebug/Port.tsx new file mode 100644 index 00000000000..da0b561988b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PlaneMasterDebug/Port.tsx @@ -0,0 +1,106 @@ +import { Box, Button, Floating, Stack } from 'tgui-core/components'; +import { classes } from 'tgui-core/react'; + +import { BlendColors, Filter, Plane, Relay } from './types'; +import { usePlaneDebugContext } from './usePlaneDebug'; + +export type PortProps = { + connection: Filter | Relay; + source?: boolean; + target_ref: (element: HTMLElement) => void; +}; + +export function Port(props: PortProps) { + const { connection, source, target_ref } = props; + const { setConnectionHighlight, act } = usePlaneDebugContext(); + const sourcePlane: Plane = ( + source ? connection.source : connection.target + ) as Plane; + const connectedPlane: Plane = ( + source ? connection.target : connection.source + ) as Plane; + return ( + + Connected to {connectedPlane.name} + {!!(connection.blend_mode !== undefined) && ( + Blend mode: {connection.blend_mode} + )} + {!!('type' in connection) && ( + Filter type: {connection.type} + )} + + + } + placement="bottom" + contentClasses="Tooltip__Port" + > + { + setConnectionHighlight({ + source: sourcePlane.plane, + target: connectedPlane.plane, + }); + }} + onMouseLeave={() => { + setConnectionHighlight(undefined); + }} + > + + + + + + + + + ); +} diff --git a/tgui/packages/tgui/interfaces/PlaneMasterDebug/index.tsx b/tgui/packages/tgui/interfaces/PlaneMasterDebug/index.tsx new file mode 100644 index 00000000000..36a8313fe2f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PlaneMasterDebug/index.tsx @@ -0,0 +1,619 @@ +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { + Button, + Dropdown, + InfinitePlane, + Stack, + Tooltip, +} from 'tgui-core/components'; + +import { resolveAsset } from '../../assets'; +import { useBackend } from '../../backend'; +import { Window } from '../../layouts'; +import { Connection, Connections, Position } from './../common/Connections'; +import { ABSOLUTE_Y_OFFSET } from './../IntegratedCircuit/constants'; +import { PlaneEditor } from './PlaneEditor'; +import { PlaneMaster } from './PlaneMaster'; +import { PlaneMenus } from './PlaneMenus'; +import { + Filter, + Plane, + PlaneConnectionsMap, + PlaneConnectorElement, + PlaneConnectorsMap, + PlaneData, + PlaneDebugData, + PlaneHighlight, + PlaneMap, + PlaneTargetMap, + Relay, +} from './types'; +import { PlaneDebugContext } from './usePlaneDebug'; + +function getPosition(el: HTMLElement | null): Position { + let xPos = 0; + let yPos = 0; + + while (el !== null) { + xPos += el.offsetLeft; + yPos += el.offsetTop; + el = el.offsetParent as HTMLElement | null; + } + + return { + x: xPos, + y: yPos + ABSOLUTE_Y_OFFSET, + }; +} + +function isDefined(x: T | undefined): x is T { + return x !== undefined; +} + +function evaluatePlaneDepth(plane: Plane, depth: number) { + let checkDepth = 0; + let toCheck = (plane.outgoing_filters as (Filter | Relay)[]) + .concat(plane.outgoing_relays) + .map((connection: Filter | Relay) => connection.target); + const allElems: Plane[] = []; + let foundChild = false; + while (checkDepth <= 2) { + let newCheck: Plane[] = []; + for (let i = 0; i < toCheck.length; i++) { + const checkElem = toCheck[i]; + if (checkElem === undefined) { + continue; + } + + if (allElems.includes(checkElem)) { + foundChild = true; + break; + } + + allElems.push(checkElem); + newCheck = newCheck.concat( + ((checkElem as Plane).outgoing_filters as (Filter | Relay)[]) + .concat(checkElem.outgoing_relays) + .map((connection: Filter | Relay) => connection.target) + .filter(isDefined), + ); + } + + if (foundChild) { + break; + } + + toCheck = newCheck; + checkDepth++; + } + + // If our plane has at least 2 children over 2 degrees of separation away + // from each other, give them a bump in depth to split them from other + // parents of those planes visually a bit + if (checkDepth) { + depth += 1; + } + + plane.depth = Math.max(depth, plane.depth); + for (let i = 0; i < plane.parents.length; i++) { + evaluatePlaneDepth(plane.parents[i], depth + 1); + } +} + +// Stolen wholesale from fontcode +function textWidth(text: string, font: string, fontsize: number) { + // default font height is 12 in tgui + font = `${fontsize}px ${font}`; + const c = document.createElement('canvas'); + const ctx = c.getContext('2d') as CanvasRenderingContext2D; + ctx.font = font; + return ctx.measureText(text).width; +} + +function getPlaneHeight(plane: Plane) { + return ( + 45 + + 19 * + Math.max( + plane.incoming_filters.length + plane.incoming_relays.length, + plane.outgoing_filters.length + plane.outgoing_relays.length + 1, + ) + + 15 + ); +} + +function getDesiredPlanePosition( + plane: Plane, + curStack: number, + tallestStack: number, +) { + const dependents: Plane[] = ( + curStack > tallestStack + ? (plane.outgoing_filters as (Filter | Relay)[]).concat( + plane.outgoing_relays, + ) + : (plane.incoming_filters as (Filter | Relay)[]).concat( + plane.incoming_relays, + ) + ) + .map((connection: Filter | Relay) => + curStack > tallestStack ? connection.target : connection.source, + ) + .filter(isDefined); + + const avgY = + dependents + .map((x) => x.position.y + getPlaneHeight(x) / 2) + .reduce((a, b) => a + b, 0) / dependents.length; + + return avgY - getPlaneHeight(plane) / 2; +} + +function mapPlanes(planes: PlaneData[]) { + const planeGraph: PlaneMap = {}; + const planeTargets: PlaneTargetMap = {}; + + for (let i = 0; i < planes.length; i++) { + const planeInfo = planes[i]; + const plane: Plane = { + name: planeInfo.name, + documentation: planeInfo.documentation, + plane: planeInfo.plane, + offset: planeInfo.offset, + real_plane: planeInfo.real_plane, + renders_onto: [], + blend_mode: planeInfo.blend_mode, + color: planeInfo.color, + alpha: planeInfo.alpha, + render_target: planeInfo.render_target, + force_hidden: !!planeInfo.force_hidden, + incoming_relays: [], + incoming_filters: [], + outgoing_relays: [], + outgoing_filters: [], + position: { x: 0, y: 0 }, + parents: [], + depth: 0, + }; + + planeGraph[planeInfo.plane] = plane; + if (planeInfo.render_target) { + planeTargets[planeInfo.render_target] = plane; + } + } + + for (let i = 0; i < planes.length; i++) { + const planeInfo = planes[i]; + const plane = planeGraph[planeInfo.plane]; + + for (let j = 0; j < planeInfo.relays.length; j++) { + const relayInfo = planeInfo.relays[j]; + const targetPlane = planeGraph[relayInfo.target]; + const relay: Relay = { + name: relayInfo.name, + source: plane, + target: targetPlane, + layer: relayInfo.layer, + blend_mode: relayInfo.blend_mode, + our_ref: relayInfo.our_ref, + node_color: 'blue', + }; + + plane.outgoing_relays.push(relay); + if (targetPlane !== undefined) { + targetPlane.incoming_relays.push(relay); + targetPlane.parents.push(plane); + } + } + + for (let j = 0; j < planeInfo.filters.length; j++) { + const filterInfo = planeInfo.filters[j]; + const sourcePlane = planeTargets[filterInfo.render_source]; + const filter: Filter = { + name: filterInfo.name, + target: plane, + source: sourcePlane, + type: filterInfo.type, + our_ref: filterInfo.our_ref, + blend_mode: filterInfo.blend_mode, + node_color: 'purple', + }; + + plane.incoming_filters.push(filter); + if (sourcePlane !== undefined) { + plane.parents.push(sourcePlane); + sourcePlane.outgoing_filters.push(filter); + } + } + } + + // Calculate plane depths to sort them out + for (const key in planeGraph) { + const plane = planeGraph[key]; + // Don't recursively evaluate nodes that we know will have some child + // to call this on them anyways + if ( + plane.outgoing_filters.length === 0 && + plane.outgoing_relays.length === 0 + ) { + evaluatePlaneDepth(plane, 1); + } + } + + const widthPerDepth: Record = {}; + const heightPerDepth: Record = {}; + const planeStacks: Record = {}; + let maxHeight = 0; + let tallestStack = 0; + for (const key in planeGraph) { + const plane = planeGraph[key]; + widthPerDepth[plane.depth] = Math.max( + widthPerDepth[plane.depth] || 0, + textWidth(plane.name, 'Verdana, Geneva', 12) + 30, + ); + + const newHeight = + (heightPerDepth[plane.depth] || 0) + getPlaneHeight(plane); + + heightPerDepth[plane.depth] = newHeight; + if (newHeight > maxHeight) { + maxHeight = newHeight; + tallestStack = plane.depth; + } + + if (planeStacks[plane.depth] === undefined) { + planeStacks[plane.depth] = []; + } + + planeStacks[plane.depth].push(plane); + } + + // We sort stacks based on planes that the plane bundle renders onto + // and the numerical plane value within the actual bundle + for (const key in planeStacks) { + let stack: Plane[] = planeStacks[key]; + stack = stack.sort((first, second) => { + const firstChildren: Plane[] = ( + first.outgoing_filters as (Filter | Relay)[] + ) + .concat(first.outgoing_relays) + .map((connection: Filter | Relay) => connection.target) + .filter(isDefined) + .sort((a, b) => a.plane - b.plane); + + const secondChildren: Plane[] = ( + second.outgoing_filters as (Filter | Relay)[] + ) + .concat(second.outgoing_relays) + .map((connection: Filter | Relay) => connection.target) + .filter(isDefined) + .sort((a, b) => a.plane - b.plane); + + // We have same children or none at all, sort ourselves based on our real planes + if ( + firstChildren.length === 0 || + secondChildren.length === 0 || + firstChildren.map((x) => x.plane).join('-') === + secondChildren.map((x) => x.plane).join('-') + ) { + return first.plane - second.plane; + } + + // planeStacks is a Record and thus automatically sorts itself + // so we can always assume that our children have already been sorted + const firstAvg = + firstChildren + .map((x) => planeStacks[x.depth].indexOf(x) || 0) + .reduce((a, b) => a + b, 0) / firstChildren.length; + + const secondAvg = + secondChildren + .map((x) => planeStacks[x.depth].indexOf(x) || 0) + .reduce((a, b) => a + b, 0) / secondChildren.length; + + if (firstAvg !== secondAvg) { + return firstAvg - secondAvg; + } + + // In a scenario where averages of our children's vertical positions match + // we want to keep all planes leading to same children grouped together + for ( + let i = 0; + i < Math.min(firstChildren.length, secondChildren.length); + i++ + ) { + const firstChild: Plane = firstChildren[i] as Plane; + const secondChild: Plane = secondChildren[i] as Plane; + + if (firstChild.plane !== secondChild.plane) { + return firstChild.plane - secondChild.plane; + } + } + + return 0; + }); + + planeStacks[key] = stack; + } + + let baseX = 0; + for (const key in planeStacks) { + const stack: Plane[] = planeStacks[key]; + for (let i = 0; i < stack.length; i++) { + const plane: Plane = stack[i]; + plane.position.x = baseX; + } + + baseX -= widthPerDepth[key] + 150; + } + + const stackKeys = Object.keys(planeStacks).sort( + (a, b) => Math.abs(+a - tallestStack) - Math.abs(+b - tallestStack), + ); + + for (let k = 0; k < stackKeys.length; k++) { + const key: number = +stackKeys[k]; + const stack: Plane[] = planeStacks[key]; + + let stackHeight = 0; + for (let i = 0; i < stack.length; i++) { + const plane: Plane = stack[i]; + const height = getPlaneHeight(plane); + + if (key === tallestStack) { + plane.position.y = stackHeight; + stackHeight += height; + continue; + } + + const desiredPos = getDesiredPlanePosition(plane, +key, tallestStack); + + if (i === 0 && desiredPos < stackHeight) { + stackHeight = desiredPos; + } else if (desiredPos > stackHeight) { + let curBottom = desiredPos + height; + let pushedPosition = 0; + + if (i < stack.length - 1) { + for (let j = i + 1; j < stack.length; j++) { + const otherPlane: Plane = stack[j]; + const otherPos = getDesiredPlanePosition( + otherPlane, + +key, + tallestStack, + ); + + if (Number.isNaN(otherPos)) { + continue; + } + + const otherHeight = getPlaneHeight(otherPlane); + curBottom += otherHeight; + pushedPosition = Math.max( + pushedPosition, + curBottom - otherPos - otherHeight / 2, + ); + } + } + + stackHeight = Math.max(desiredPos - pushedPosition / 2, stackHeight); + } + + plane.position.y = stackHeight; + stackHeight += height; + } + } + + return planeGraph; +} + +export function PlaneMasterDebug() { + const { data, act } = useBackend(); + const { + mob_name, + planes, + tracking_active, + mob_ref, + our_ref, + enable_group_view, + our_group, + present_groups, + } = data; + const connectionDom = useRef({}); + + const planesProcessed = useMemo(() => mapPlanes(planes), [planes]); + + const [connectionData, setConnectionData] = useState({}); + const [connectionHighlight, setConnectionHighlight] = + useState(); + + useLayoutEffect(() => { + const doms = connectionDom.current; + const newConnectionData: PlaneConnectionsMap = {}; + for (const our_ref in doms) { + const connection: PlaneConnectorElement = doms[our_ref]; + if (connection === undefined) { + continue; + } + if (connection.input === undefined || connection.output === undefined) { + continue; + } + newConnectionData[our_ref] = { + input: getPosition(connection.input), + output: getPosition(connection.output), + }; + } + setConnectionData(newConnectionData); + }, [planes]); + + const connections: Connection[] = []; + for (const key in planesProcessed) { + const plane = planesProcessed[key]; + for (let i = 0; i < plane.outgoing_filters.length; i++) { + const filter = plane.outgoing_filters[i]; + const targetPlane = filter.target; + if ( + targetPlane === undefined || + connectionData[filter.our_ref] === undefined + ) { + continue; + } + + const highlighted = + (plane.plane === connectionHighlight?.target && + targetPlane.plane === connectionHighlight?.source) || + (targetPlane.plane === connectionHighlight?.target && + plane.plane === connectionHighlight?.source); + + connections.push({ + color: highlighted ? 'white' : 'purple', + from: connectionData[filter.our_ref].output, + to: connectionData[filter.our_ref].input, + ref: plane.name, + }); + } + + for (let i = 0; i < plane.outgoing_relays.length; i++) { + const relay = plane.outgoing_relays[i]; + const targetPlane = relay.target; + if ( + targetPlane === undefined || + connectionData[relay.our_ref] === undefined + ) { + continue; + } + + const highlighted = + (plane.plane === connectionHighlight?.target && + targetPlane.plane === connectionHighlight?.source) || + (targetPlane.plane === connectionHighlight?.target && + plane.plane === connectionHighlight?.source); + + connections.push({ + color: highlighted ? 'white' : 'blue', + from: connectionData[relay.our_ref].output, + to: connectionData[relay.our_ref].input, + ref: plane.name, + }); + } + } + + // Must be a number as Plane objects are recreated whenever plane data changes + const [activePlane, setActivePlane] = useState(); + const [connectionOpen, setConnectionOpen] = useState(false); + const [infoOpen, setInfoOpen] = useState(false); + const [planeOpen, setPlaneOpen] = useState(false); + + return ( + + + {!!enable_group_view && ( + + + act('set_group', { target_group: value }) + } + /> + + )} + +