Planemaster Debugger Refactor (#91094)

## About The Pull Request

Rewrote the planemaster debugger to be more legible and useful. Planes
are now clustered based on their dependants, all buttons have been moved
to the header, connections are highlighted when hovering over nodes and
you can see filter type and blend mode when clicking the node (deleting
a connection is done through a button in the tooltip)


![HIvL79iDn0](https://github.com/user-attachments/assets/b6a94c01-8f3b-416e-9fd2-e91cb4499740)

## Why It's Good For The Game

Old planemaster debugger is horrifically jank to use, has broken visual
offsets for all nodes, connections and buttons, and is in a single
thousand line long file.

## Changelog
🆑
refactor: Refactored the planemaster debugger tool
/🆑
This commit is contained in:
SmArtKar
2025-05-15 22:51:19 -07:00
committed by GitHub
parent 59ada060b2
commit 13e1c49c7d
14 changed files with 1500 additions and 1316 deletions
+1 -1
View File
@@ -210,7 +210,7 @@
name = "Lighting plate"
documentation = "Anything on this plane will be <b>multiplied</b> with the plane it's rendered onto (typically the game plane).\
<br>That's how lighting functions at base. Because it uses BLEND_MULTIPLY and occasionally color matrixes, it needs a backdrop of blackness.\
<br>See <a href=\"https://secure.byond.com/forum/?post=2141928\">This byond post</a>\
<br>See <a href=\"https://secure.byond.com/forum/?post=2141928\">this byond post</a>\
<br>Lemme see uh, we're masked by the emissive plane so it can actually function (IE: make things glow in the dark).\
<br>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.\
<br>Masks us out so it has the breathing room to apply its effect.\
+49 -193
View File
@@ -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]"]
+1 -1
View File
@@ -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],
+2 -2
View File
@@ -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;
@@ -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]);
File diff suppressed because it is too large Load Diff
@@ -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 (
<Section
fill
scrollable
width="450px"
position="absolute"
top="0px"
right="0px"
backgroundColor="#121212"
title={`Plane Master: ${currentPlane.name}`}
buttons={
<Button
icon="times"
tooltip="Close"
onClick={() => setPlaneOpen(false)}
/>
}
>
<Section title="Information">
<Box dangerouslySetInnerHTML={doc_html} />
<br />
<LabeledList>
<LabeledList.Divider />
<Tooltip
content="Any atoms in the world with the same plane will be drawn to this plane master"
position="right"
>
<LabeledList.Item label="Plane">
{currentPlane.plane}
</LabeledList.Item>
</Tooltip>
<Tooltip
content="You can think of this as the 'layer' this plane is on. We make duplicates of each plane for each layer, so we can make multiz work"
position="right"
>
<LabeledList.Item label="Offset">
{currentPlane.offset}
</LabeledList.Item>
</Tooltip>
<Tooltip
content="Render targets can be used to either reference or draw existing drawn items on the map. For plane masters, we use these for either relays (the blue lines), or filters (the pink ones)"
position="right"
>
<LabeledList.Item label="Render Target">
{currentPlane.render_target
? `"${currentPlane.render_target}"`
: 'None'}
</LabeledList.Item>
</Tooltip>
<Tooltip
content="Defines how this plane draws to the things it is relay'd onto. Check the byond ref for more details"
position="right"
>
<LabeledList.Item label="Blend Mode">
{currentPlane.blend_mode}
</LabeledList.Item>
</Tooltip>
<Tooltip
content="If this is 1, the plane master is being forced to hide from its mob. This is most often done as an optimization tactic, since some planes only rarely need to be used"
position="right"
>
<LabeledList.Item label="Forced Hidden">
{currentPlane.force_hidden ? 'True' : 'False'}
</LabeledList.Item>
</Tooltip>
</LabeledList>
<br />
<Section title="Visuals">
<Button
tooltip="Open this plane's VV menu"
mr="5px"
mb="5px"
onClick={() =>
act('vv_plane', {
edit: currentPlane.plane,
})
}
>
View Variables
</Button>
<Button
tooltip="Apply and edit effects over the whole plane"
mr="5px"
mb="5px"
onClick={() =>
act('edit_filters', {
edit: currentPlane.plane,
})
}
>
Edit Filters
</Button>
<Button
tooltip="Modify how different color components map to the final plane"
mr="5px"
mb="5px"
onClick={() =>
act('edit_color_matrix', {
edit: currentPlane.plane,
})
}
>
Edit Color Matrix
</Button>
<Slider
value={currentPlane.alpha}
minValue={0}
maxValue={255}
step={1}
stepPixelSize={1.9}
onDrag={(_event, value) =>
act('set_alpha', { edit: currentPlane.plane, alpha: value })
}
onChange={(_event, value) =>
act('set_alpha', { edit: currentPlane.plane, alpha: value })
}
>
Alpha ({currentPlane.alpha})
</Slider>
</Section>
</Section>
</Section>
);
}
@@ -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 (
<Box
position="absolute"
left={`${plane.position.x}px`}
top={`${plane.position.y}px`}
minWidth="150px"
style={
connectionHighlight?.target === plane.plane ||
activePlane === plane.plane
? {
outline: '2px outset hsl(0, 0%, 85%)',
borderTopLeftRadius: 'var(--border-radius-huge)',
borderTopRightRadius: 'var(--border-radius-huge)',
}
: {}
}
>
<Box
backgroundColor={plane.force_hidden ? '#191919' : '#000000'}
py={1}
px={1}
className="ObjectComponent__Titlebar"
style={
plane.force_hidden
? {
borderBottom: '2px dotted rgba(255, 255, 255, 0.8)',
}
: {}
}
>
<Stack>
<Stack.Item grow>{plane.name}</Stack.Item>
<Stack.Item>
<Button
icon="pager"
compact
tooltip="Inspect and edit this plane"
onClick={() => {
setActivePlane(plane.plane);
if (planeOpen) {
return;
}
const windowPosition = getWindowPosition();
windowPosition[0] -= 150;
setWindowPosition(windowPosition);
setPlaneOpen(true);
}}
/>
</Stack.Item>
</Stack>
</Box>
<Box
className={classes([
plane.force_hidden
? 'ObjectComponent__Greyed_Content'
: 'ObjectComponent__Content',
])}
py={1}
px={1}
>
<Stack>
<Stack.Item>
<Stack vertical>
{incoming_connections.map((connection: Filter | Relay) => (
<Stack.Item key={connection.our_ref}>
<Port
connection={connection}
target_ref={(element) => {
if (connectionData[connection.our_ref] === undefined) {
connectionData[connection.our_ref] = {};
}
connectionData[connection.our_ref].input = element;
}}
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
<Stack.Item grow />
<Stack.Item>
<Stack vertical>
{outgoing_connections.map((connection: Filter | Relay) => (
<Stack.Item key={connection.our_ref} align="flex-end">
<Port
connection={connection}
source
target_ref={(element) => {
if (connectionData[connection.our_ref] === undefined) {
connectionData[connection.our_ref] = {};
}
connectionData[connection.our_ref].output = element;
}}
/>
</Stack.Item>
))}
<Stack.Item align="flex-end">
<Button
icon="plus"
compact
onClick={() => {
setActivePlane(plane.plane);
setConnectionOpen(true);
}}
tooltip="Connect to another plane"
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Box>
</Box>
);
}
@@ -0,0 +1,246 @@
import { useState } from 'react';
import { Button, Dropdown, Modal, Section, Stack } from 'tgui-core/components';
import { BlendModes, Plane } from './types';
import { usePlaneDebugContext } from './usePlaneDebug';
export function PlaneMenus() {
const { connectionOpen, infoOpen } = usePlaneDebugContext();
return (
<>
{!!connectionOpen && <AddConnectionModal />}
{!!infoOpen && <InfoModal />}
</>
);
}
function AddConnectionModal() {
const {
activePlane,
setActivePlane,
setConnectionOpen,
planesProcessed,
act,
} = usePlaneDebugContext();
const currentPlane = planesProcessed[activePlane as number];
const optionMap: Record<string, number> = {};
const [selectedTarget, setSelectedTarget] = useState<number>();
const [selectedBlend, setSelectedBlend] = useState<string>('BLEND_DEFAULT');
const selectablePlanes: Plane[] = [];
for (const key in planesProcessed) {
const plane: Plane = planesProcessed[key];
if (plane !== currentPlane) {
selectablePlanes.push(plane);
optionMap[plane.name] = plane.plane;
}
}
const planeOptions: Array<string> = selectablePlanes
.sort((a, b) => {
if (a.depth !== b.depth) {
return a.depth - b.depth;
}
return a.plane - b.plane;
})
.map((a) => a.name);
return (
<Modal p={1}>
<Section
fill
title={`Add relay from ${currentPlane.name}`}
buttons={
<Button
icon="close"
color="bad"
onClick={() => {
setConnectionOpen(false);
setActivePlane(undefined);
}}
/>
}
>
<Stack fill vertical>
<Stack.Item>
<Dropdown
options={planeOptions}
selected={
selectedTarget !== undefined
? planesProcessed[selectedTarget].name
: 'Select target'
}
width="300px"
onSelected={(value) => setSelectedTarget(optionMap[value])}
/>
</Stack.Item>
<Stack.Item>
<Dropdown
options={Object.keys(BlendModes).filter((x) =>
Number.isNaN(Number(x)),
)}
selected={selectedBlend}
width="300px"
onSelected={(value) => setSelectedBlend(value)}
/>
</Stack.Item>
<Stack.Item textAlign="center">
<Button
color="good"
onClick={() => {
act('connect_relay', {
source: activePlane,
target: selectedTarget,
mode: BlendModes[selectedBlend],
});
setConnectionOpen(false);
setActivePlane(undefined);
}}
>
Confirm
</Button>
</Stack.Item>
</Stack>
</Section>
</Modal>
);
}
function InfoModal() {
const { setInfoOpen } = usePlaneDebugContext();
return (
<Modal
position="absolute"
top="100px"
right="180px"
left="180px"
bottom="100px"
>
<Section
fill
scrollable
title="Information Panel"
buttons={
<Button
icon="times"
tooltip="Close"
onClick={() => setInfoOpen(false)}
/>
}
>
<h3>What is all this?</h3>
This UI exists to help visualize plane masters, the backbone of our
rendering system. <br />
It also provices some tools for editing and messing with them. <br />
<br />
<h3>How to use this UI</h3> <br />
This UI exists primarially as a visualizer, mostly because this info is
quite obscure, and I want it to be easier to understand.
<br />
<br />
That said, it also supports editing plane masters, adding and removing
relays, and provides easy access to color matrix/filter/alpha/vv
editing. <br />
<br />
To start off with, each little circle represents a{' '}
<code>render_target</code> based connection.
<br />
Blue nodes are relays, so drawing one plane onto another. Purple ones
are filter based connections. <br />
You can tell where a node starts and ends based on the side of the plane
it&apos;s on. <br />
<br />
Adding a new relay is simple, you just need to hit the + button, and
select a plane by name to relay onto. <br />
<br />
Each plane can be viewed more closely by clicking the little button in
it&apos;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. <br />
<br />
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.{' '}
<br />
<br />
<h3>What is a plane master?</h3>
You can think of a plane master as a way to group a set of objects onto
one rendering slate. <br />
It is per client too, which makes it quite powerful. This is done using
the <code>plane</code> variable of <code>/atom</code>. <br />
<br />
We first create an atom with an appearance flag that contains{' '}
<code>PLANE_MASTER</code> and give it a <code>plane</code> value. <br />
Then we mirror the same <code>plane</code> value on all the atoms we
want to render in this group.
<br />
<br />
Finally, we place the <code>PLANE_MASTER</code>&apos;d atom in the
relevent client&apos;s screen contents. <br />
That sets up the bare minimum.
<br />
<br />
It is worth noting that the <code>plane</code> var does not only effect
this rendering grouping behavior. <br />
It also effects the layering of objects on the map. <br />
<br />
For this reason, there are some effects that are pretty much impossible
with planes. <br />
Masking one thing while also drawing that thing in the correct order
with other objects on the map is a good example of this.
<br />
It <b>is</b> possible to do, but it&apos;s quite disruptive.
<br />
<br />
Normally, planes will just group, apply an effect, and then draw
directly to the game.
<br />
What if we wanted to draw <b>planes</b> onto other planes then? <br />
<br />
<h3>Render Targets and Relays</h3>
<br />
Rendering one thing onto another is actually not that complex. <br />
We can set the <code>render_target</code> variable of an atom to relay
it to some <code>render_source</code>.<br />
<br />
If that <code>render_target</code> is preceeded by *, it will
<b>not</b> be drawn to the actual client view, and instead just relayed.{' '}
<br />
<br />
Ok so we can relay a plane master onto some other atom, but how do we
get it on another plane master? We can&apos;t just draw it with{' '}
<code>render_source</code>, since we might want to relay more then one
plane master.
<br />
<br />
Why not relay it to another atom then? and then well, set that
atom&apos;s <code>plane</code> var to the plane master we want? <br />
<br />
That ends up being about what we do. <br />
It&apos;s worth noting that render sources are often used by filters,
normally to apply some displacement or mask.
<br />
<br />
<h3>Applying effects</h3> <br />
Ok so we can group and relay planes, but what can we actually do with
that? <br />
<br />
Lots of stuff it turns out. Filters are quite powerful, and we use them
quite a bit. <br />
You can use filters to mask one plane with another, or use one plane as
a distortion source for another. <br />
<br />
Can do more basic stuff too, setting a plane&apos;s color matrix can be
quite powerful. <br />
Even just setting alpha to show and hide things can be quite useful.{' '}
<br />
<br />
I won&apos;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. <br />
<br />
</Section>
</Modal>
);
}
@@ -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 (
<Floating
content={
<Stack fill vertical>
<Stack.Item>Connected to {connectedPlane.name}</Stack.Item>
{!!(connection.blend_mode !== undefined) && (
<Stack.Item>Blend mode: {connection.blend_mode}</Stack.Item>
)}
{!!('type' in connection) && (
<Stack.Item>Filter type: {connection.type}</Stack.Item>
)}
<Button
color="bad"
width="120px"
onClick={() => {
if ('type' in connection) {
act('disconnect_filter', {
target: connection.target?.plane,
name: connection.name,
});
} else {
act('disconnect_relay', {
source: connection.source?.plane,
target: connection.target?.plane,
});
}
}}
>
Delete connection
</Button>
</Stack>
}
placement="bottom"
contentClasses="Tooltip__Port"
>
<Box
className={classes(['ObjectComponent__Port'])}
textAlign="center"
onMouseOver={() => {
setConnectionHighlight({
source: sourcePlane.plane,
target: connectedPlane.plane,
});
}}
onMouseLeave={() => {
setConnectionHighlight(undefined);
}}
>
<svg
style={{
width: '100%',
height: '100%',
}}
viewBox="0, 0, 100, 100"
>
<circle
stroke={connection.node_color}
strokeDasharray={`${100 * Math.PI}`}
strokeDashoffset={-100 * Math.PI}
className={`color-stroke-${connection.node_color}`}
strokeWidth="50px"
cx="50"
cy="50"
r="50"
fillOpacity="0"
transform="rotate(90, 50, 50)"
/>
<circle
cx="50"
cy="50"
r="50"
className={`color-fill-${connection.node_color}`}
/>
<circle
cx="50"
cy="50"
r="25"
className={`color-fill-${BlendColors[connection.blend_mode || 'BLEND_DEFAULT'] || connection.node_color}`}
/>
</svg>
<span ref={target_ref} className="ObjectComponent__PortPos" />
</Box>
</Floating>
);
}
@@ -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<T>(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<number, number> = {};
const heightPerDepth: Record<number, number> = {};
const planeStacks: Record<number, Plane[]> = {};
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<PlaneDebugData>();
const {
mob_name,
planes,
tracking_active,
mob_ref,
our_ref,
enable_group_view,
our_group,
present_groups,
} = data;
const connectionDom = useRef<PlaneConnectorsMap>({});
const planesProcessed = useMemo(() => mapPlanes(planes), [planes]);
const [connectionData, setConnectionData] = useState<PlaneConnectionsMap>({});
const [connectionHighlight, setConnectionHighlight] =
useState<PlaneHighlight>();
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<number>();
const [connectionOpen, setConnectionOpen] = useState<boolean>(false);
const [infoOpen, setInfoOpen] = useState<boolean>(false);
const [planeOpen, setPlaneOpen] = useState<boolean>(false);
return (
<PlaneDebugContext.Provider
value={{
connectionHighlight,
setConnectionHighlight,
activePlane,
setActivePlane,
connectionOpen,
setConnectionOpen,
infoOpen,
setInfoOpen,
planeOpen,
setPlaneOpen,
planesProcessed,
act,
}}
>
<Window
width={planeOpen ? 1500 : 1200}
height={800}
title={`Plane Debugging: ${mob_name}`}
buttons={
<Stack>
{!!enable_group_view && (
<Tooltip
content="Plane masters are stored in groups, based off where they came from. MAIN is the main group, but if you open something that displays atoms in a new window, it'll show up here."
position="right"
>
<Dropdown
options={present_groups}
selected={our_group}
onSelected={(value) =>
act('set_group', { target_group: value })
}
/>
</Tooltip>
)}
<Stack.Item>
<Button
color="transparent"
tooltip="Debugger Documentation"
icon="question"
selected={infoOpen}
onClick={() => setInfoOpen(true)}
/>
</Stack.Item>
{!!(mob_ref !== our_ref) && (
<Stack.Item>
<Button
color="transparent"
tooltip="Reset Mob Focus"
icon="magnifying-glass"
onClick={() => act('reset_mob')}
/>
</Stack.Item>
)}
<Stack.Item>
<Button
color="transparent"
tooltip="View Mirroring"
icon={our_ref !== mob_ref ? 'ghost' : 'eye'}
selected={tracking_active}
onClick={() => act('toggle_mirroring')}
/>
</Stack.Item>
<Stack.Item>
<Button
color="transparent"
tooltip="View Mob Variables"
icon="pen"
onClick={() => act('vv_mob')}
/>
</Stack.Item>
<Stack.Item>
<Button
color="transparent"
tooltip="Rebuild Plane Masters"
icon="recycle"
onClick={() => act('rebuild')}
/>
</Stack.Item>
</Stack>
}
>
<Window.Content
style={{
backgroundImage: 'none',
}}
>
<InfinitePlane
width="100%"
height="100%"
backgroundImage={resolveAsset('grid_background.png')}
imageWidth={900}
initialLeft={500}
initialTop={-1350}
>
{planes.map((plane) => (
<PlaneMaster
key={plane.name}
plane={planesProcessed[plane.plane]}
connectionData={connectionDom.current}
/>
))}
<Connections connections={connections} />
</InfinitePlane>
{!!planeOpen && <PlaneEditor />}
<PlaneMenus />
</Window.Content>
</Window>
</PlaneDebugContext.Provider>
);
}
@@ -0,0 +1,131 @@
import { BooleanLike } from 'tgui-core/react';
import { Position } from './../common/Connections';
export type PlaneDebugData = {
mob_name: string;
mob_ref: string;
our_ref: string;
tracking_active: BooleanLike;
enable_group_view: BooleanLike;
our_group: string;
present_groups: string[];
planes: PlaneData[];
};
export type PlaneData = {
name: string;
documentation: string;
plane: number;
offset: number;
real_plane: number;
renders_onto: number[];
blend_mode: string;
color: string | number[];
alpha: number;
render_target: string;
force_hidden: BooleanLike;
relays: RelayData[];
filters: FilterData[];
};
export type RelayData = {
name: string;
source: number;
target: number;
layer: number;
blend_mode: string;
our_ref: string;
};
export type FilterData = {
name: string;
render_source: string;
our_ref: string;
type: string;
// For layering filters
blend_mode?: string;
};
export type Plane = {
name: string;
documentation: string;
plane: number;
offset: number;
real_plane: number;
renders_onto: Plane[];
blend_mode: string;
color: string | number[];
alpha: number;
render_target: string;
force_hidden: boolean;
incoming_relays: Relay[];
incoming_filters: Filter[];
outgoing_relays: Relay[];
outgoing_filters: Filter[];
position: Position;
parents: Plane[];
depth: number;
};
export type Relay = {
name: string;
source?: Plane;
target?: Plane;
layer: number;
blend_mode: string;
our_ref: string;
node_color: string;
};
export type Filter = {
name: string;
source?: Plane;
target?: Plane;
our_ref: string;
type: string;
// For layering filters
blend_mode?: string;
node_color: string;
};
export const BlendColors = {
BLEND_DEFAULT: undefined,
BLEND_OVERLAY: 'white',
BLEND_ADD: 'olive',
BLEND_SUBTRACT: 'red',
BLEND_MULTIPLY: 'orange',
BLEND_INSET_OVERLAY: 'teal',
};
export enum BlendModes {
'BLEND_DEFAULT',
'BLEND_OVERLAY',
'BLEND_ADD',
'BLEND_SUBTRACT',
'BLEND_MULTIPLY',
'BLEND_INSET_OVERLAY',
}
export type PlaneMap = Record<number, Plane>;
export type PlaneTargetMap = Record<string, Plane>;
export type PlaneConnectionsMap = Record<string, PlaneConnection>;
export type PlaneConnectorsMap = Record<string, PlaneConnectorElement>;
export type PlaneConnectorElement = {
// Both of these are relay/filter ref -> HTMLElement for that input/output
input?: HTMLElement;
output?: HTMLElement;
};
export type PlaneConnection = {
// Both of these are relay/filter ref -> coordinates for that input/output
input: Position;
output: Position;
};
export type PlaneHighlight = {
// Must be numbers as actual Plane objects are constantly regenerated
source: number;
target: number;
};
@@ -0,0 +1,24 @@
import { createContext, Dispatch, SetStateAction, useContext } from 'react';
import { PlaneHighlight, PlaneMap } from './types';
type PlaneDebug = {
connectionHighlight: PlaneHighlight | undefined;
setConnectionHighlight: Dispatch<SetStateAction<PlaneHighlight | undefined>>;
activePlane: number | undefined;
setActivePlane: Dispatch<SetStateAction<number | undefined>>;
connectionOpen: boolean;
setConnectionOpen: Dispatch<SetStateAction<boolean>>;
infoOpen: boolean;
setInfoOpen: Dispatch<SetStateAction<boolean>>;
planeOpen: boolean;
setPlaneOpen: Dispatch<SetStateAction<boolean>>;
planesProcessed: PlaneMap;
act: Function;
};
export const PlaneDebugContext = createContext({} as PlaneDebug);
export function usePlaneDebugContext() {
return useContext(PlaneDebugContext);
}
@@ -54,6 +54,17 @@ $map-keys: colors.$color-map !default;
inset: 0;
}
.Tooltip__Port {
backdrop-filter: var(--tooltip-blur);
background-color: var(--tooltip-background);
border-radius: var(--tooltip-border-radius);
box-shadow: 0.1em 0.1em 1.25em -0.1em hsla(0, 0%, 0%, 0.5);
color: var(--tooltip-color);
max-width: base.em(250px);
padding: var(--space-m) var(--space-l);
text-align: left;
}
@each $color-name, $color-value in $map-keys {
.color-stroke-#{$color-name} {
stroke: hsl(from $color-value h s calc(l + var(--adjust-color))) !important;