mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +00:00
[MIRROR] Switch circuits to a grid visual coding system (#9173)
Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: CHOMPStation2 <chompsation2@gmail.com>
This commit is contained in:
4
code/modules/asset_cache/assets/circuits.dm
Normal file
4
code/modules/asset_cache/assets/circuits.dm
Normal file
@@ -0,0 +1,4 @@
|
||||
/datum/asset/simple/circuit_assets
|
||||
assets = list(
|
||||
"grid_background.png" = 'icons/UI_Icons/tgui/grid_background.png',
|
||||
)
|
||||
@@ -60,6 +60,11 @@
|
||||
ui = new(user, src, "ICAssembly", name, parent_ui)
|
||||
ui.open()
|
||||
|
||||
/obj/item/electronic_assembly/ui_assets(mob/user)
|
||||
return list(
|
||||
get_asset_datum(/datum/asset/simple/circuit_assets)
|
||||
)
|
||||
|
||||
/obj/item/electronic_assembly/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/list/data = ..()
|
||||
|
||||
@@ -78,19 +83,10 @@
|
||||
data["battery_max"] = round(battery?.maxcharge, 0.1)
|
||||
data["net_power"] = net_power / CELLRATE
|
||||
|
||||
// This works because lists are always passed by reference in BYOND, so modifying unremovable_circuits
|
||||
// after setting data["unremovable_circuits"] = unremovable_circuits also modifies data["unremovable_circuits"]
|
||||
// Same for the removable one
|
||||
var/list/unremovable_circuits = list()
|
||||
data["unremovable_circuits"] = unremovable_circuits
|
||||
var/list/removable_circuits = list()
|
||||
data["removable_circuits"] = removable_circuits
|
||||
var/list/circuits = list()
|
||||
for(var/obj/item/integrated_circuit/circuit in contents)
|
||||
var/list/target = circuit.removable ? removable_circuits : unremovable_circuits
|
||||
target.Add(list(list(
|
||||
"name" = circuit.displayed_name,
|
||||
"ref" = REF(circuit),
|
||||
)))
|
||||
UNTYPED_LIST_ADD(circuits, circuit.tgui_data(user, ui, state))
|
||||
data["circuits"] = circuits
|
||||
|
||||
return data
|
||||
|
||||
@@ -98,8 +94,6 @@
|
||||
if(..())
|
||||
return TRUE
|
||||
|
||||
var/obj/held_item = usr.get_active_hand()
|
||||
|
||||
switch(action)
|
||||
// Actual assembly actions
|
||||
if("rename")
|
||||
@@ -118,6 +112,48 @@
|
||||
return TRUE
|
||||
|
||||
// Circuit actions
|
||||
if("wire_internal")
|
||||
var/datum/integrated_io/pin1 = locate(params["pin1"])
|
||||
if(!istype(pin1))
|
||||
return
|
||||
var/datum/integrated_io/pin2 = locate(params["pin2"])
|
||||
if(!istype(pin2))
|
||||
return
|
||||
|
||||
var/obj/item/integrated_circuit/holder1 = pin1.holder
|
||||
if(!istype(holder1) || holder1.loc != src || holder1.assembly != src)
|
||||
return
|
||||
|
||||
var/obj/item/integrated_circuit/holder2 = pin2.holder
|
||||
if(!istype(holder2) || holder2.loc != src || holder2.assembly != src)
|
||||
return
|
||||
|
||||
// Wiring the same pin will unwire it
|
||||
if(pin2 in pin1.linked)
|
||||
pin1.linked -= pin2
|
||||
pin2.linked -= pin1
|
||||
else
|
||||
pin1.linked |= pin2
|
||||
pin2.linked |= pin1
|
||||
|
||||
return TRUE
|
||||
|
||||
if("remove_all_wires")
|
||||
var/datum/integrated_io/pin1 = locate(params["pin"])
|
||||
if(!istype(pin1))
|
||||
return
|
||||
|
||||
var/obj/item/integrated_circuit/holder1 = pin1.holder
|
||||
if(!istype(holder1) || holder1.loc != src || holder1.assembly != src)
|
||||
return
|
||||
|
||||
for(var/datum/integrated_io/other as anything in pin1.linked)
|
||||
other.linked -= pin1
|
||||
|
||||
pin1.linked.Cut()
|
||||
|
||||
return TRUE
|
||||
|
||||
if("open_circuit")
|
||||
var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents
|
||||
if(!istype(C))
|
||||
@@ -125,27 +161,6 @@
|
||||
C.tgui_interact(usr, null, ui)
|
||||
return TRUE
|
||||
|
||||
if("rename_circuit")
|
||||
var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents
|
||||
if(!istype(C))
|
||||
return
|
||||
C.rename_component(usr)
|
||||
return TRUE
|
||||
|
||||
if("scan_circuit")
|
||||
var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents
|
||||
if(!istype(C))
|
||||
return
|
||||
if(istype(held_item, /obj/item/integrated_electronics/debugger))
|
||||
var/obj/item/integrated_electronics/debugger/D = held_item
|
||||
if(D.accepting_refs)
|
||||
D.afterattack(C, usr, TRUE)
|
||||
else
|
||||
to_chat(usr, span_warning("The Debugger's 'ref scanner' needs to be on."))
|
||||
else
|
||||
to_chat(usr, span_warning("You need a multitool/debugger set to 'ref' mode to do that."))
|
||||
return TRUE
|
||||
|
||||
if("remove_circuit")
|
||||
var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents
|
||||
if(!istype(C))
|
||||
@@ -153,14 +168,6 @@
|
||||
C.remove(usr)
|
||||
return TRUE
|
||||
|
||||
if("bottom_circuit")
|
||||
var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents
|
||||
if(!istype(C))
|
||||
return
|
||||
// Puts it at the bottom of our contents
|
||||
// Note, this intentionally does *not* use forceMove, because forceMove will stop if it detects the same loc
|
||||
C.loc = null
|
||||
C.loc = src
|
||||
return FALSE
|
||||
// End TGUI
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ a creative player the means to solve many problems. Circuits are held inside an
|
||||
|
||||
data["name"] = name
|
||||
data["desc"] = desc
|
||||
data["ref"] = REF(src)
|
||||
data["displayed_name"] = displayed_name
|
||||
data["removable"] = removable
|
||||
|
||||
@@ -110,21 +111,7 @@ a creative player the means to solve many problems. Circuits are held inside an
|
||||
outputs_list.Add(list(tgui_pin_data(io)))
|
||||
|
||||
for(var/datum/integrated_io/io in activators)
|
||||
var/list/list/activator = list(
|
||||
"ref" = REF(io),
|
||||
"name" = io.name,
|
||||
"pulse_out" = io.data,
|
||||
"linked" = list()
|
||||
)
|
||||
for(var/datum/integrated_io/linked in io.linked)
|
||||
activator["linked"].Add(list(list(
|
||||
"ref" = REF(linked),
|
||||
"name" = linked.name,
|
||||
"holder_ref" = REF(linked.holder),
|
||||
"holder_name" = linked.holder.displayed_name,
|
||||
)))
|
||||
|
||||
activators_list.Add(list(activator))
|
||||
activators_list.Add(list(tgui_pin_data(io)))
|
||||
|
||||
data["inputs"] = inputs_list
|
||||
data["outputs"] = outputs_list
|
||||
@@ -139,6 +126,7 @@ a creative player the means to solve many problems. Circuits are held inside an
|
||||
pindata["type"] = io.display_pin_type()
|
||||
pindata["name"] = io.name
|
||||
pindata["data"] = io.display_data(io.data)
|
||||
pindata["rawdata"] = io.data
|
||||
pindata["ref"] = REF(io)
|
||||
var/list/linked_list = list()
|
||||
for(var/datum/integrated_io/linked in io.linked)
|
||||
|
||||
BIN
icons/UI_Icons/tgui/grid_background.png
Normal file
BIN
icons/UI_Icons/tgui/grid_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
222
tgui/packages/tgui/components/InfinitePlane.tsx
Normal file
222
tgui/packages/tgui/components/InfinitePlane.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
// TODO: Replace when tgui-core is fixed https://github.com/tgstation/tgui-core/issues/25
|
||||
|
||||
import { round } from 'common/math';
|
||||
import { Component, PropsWithChildren } from 'react';
|
||||
import { Button, ProgressBar, Stack } from 'tgui-core/components';
|
||||
|
||||
import { BoxProps, computeBoxProps } from './Box';
|
||||
|
||||
const ZOOM_MIN_VAL = 0.5;
|
||||
const ZOOM_MAX_VAL = 1.5;
|
||||
|
||||
const ZOOM_INCREMENT = 0.1;
|
||||
|
||||
export type InfinitePlaneProps = PropsWithChildren<
|
||||
{
|
||||
onZoomChange?: (newZoomValue: number) => void;
|
||||
onBackgroundMoved?: (newX: number, newY: number) => void;
|
||||
initialLeft?: number;
|
||||
initialTop?: number;
|
||||
backgroundImage?: string;
|
||||
imageWidth: number;
|
||||
} & BoxProps
|
||||
>;
|
||||
|
||||
type InfinitePlaneState = {
|
||||
mouseDown: boolean;
|
||||
|
||||
left: number;
|
||||
top: number;
|
||||
|
||||
lastLeft: number;
|
||||
lastTop: number;
|
||||
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
export type MouseEventExtension = {
|
||||
screenZoomX: number;
|
||||
screenZoomY: number;
|
||||
};
|
||||
|
||||
export class InfinitePlane extends Component<
|
||||
InfinitePlaneProps,
|
||||
InfinitePlaneState
|
||||
> {
|
||||
constructor(props: InfinitePlaneProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mouseDown: false,
|
||||
|
||||
left: 0,
|
||||
top: 0,
|
||||
|
||||
lastLeft: 0,
|
||||
lastTop: 0,
|
||||
|
||||
zoom: 1,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
|
||||
window.addEventListener('mousedown', this.doOffsetMouse);
|
||||
window.addEventListener('mousemove', this.doOffsetMouse);
|
||||
window.addEventListener('mouseup', this.doOffsetMouse);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
|
||||
window.removeEventListener('mousedown', this.doOffsetMouse);
|
||||
window.removeEventListener('mousemove', this.doOffsetMouse);
|
||||
window.removeEventListener('mouseup', this.doOffsetMouse);
|
||||
}
|
||||
|
||||
// This is really, REALLY cursed and basically overrides a built-in browser event via propagation rules
|
||||
doOffsetMouse = (event: MouseEvent & MouseEventExtension) => {
|
||||
const { zoom } = this.state;
|
||||
event.screenZoomX = event.screenX * Math.pow(zoom, -1);
|
||||
event.screenZoomY = event.screenY * Math.pow(zoom, -1);
|
||||
};
|
||||
|
||||
handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
mouseDown: true,
|
||||
lastLeft: event.clientX - state.left,
|
||||
lastTop: event.clientY - state.top,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onMouseUp = () => {
|
||||
this.setState({
|
||||
mouseDown: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleZoomIncrease = (event: any) => {
|
||||
const { onZoomChange } = this.props;
|
||||
const { zoom } = this.state;
|
||||
const newZoomValue = round(
|
||||
Math.min(zoom + ZOOM_INCREMENT, ZOOM_MAX_VAL),
|
||||
1,
|
||||
);
|
||||
this.setState({
|
||||
zoom: newZoomValue,
|
||||
});
|
||||
if (onZoomChange) {
|
||||
onZoomChange(newZoomValue);
|
||||
}
|
||||
};
|
||||
|
||||
handleZoomDecrease = (event: any) => {
|
||||
const { onZoomChange } = this.props;
|
||||
const { zoom } = this.state;
|
||||
const newZoomValue = round(
|
||||
Math.max(zoom - ZOOM_INCREMENT, ZOOM_MIN_VAL),
|
||||
1,
|
||||
);
|
||||
this.setState({
|
||||
zoom: newZoomValue,
|
||||
});
|
||||
|
||||
if (onZoomChange) {
|
||||
onZoomChange(newZoomValue);
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { onBackgroundMoved, initialLeft = 0, initialTop = 0 } = this.props;
|
||||
if (this.state.mouseDown) {
|
||||
let newX, newY;
|
||||
this.setState((state) => {
|
||||
newX = event.clientX - state.lastLeft;
|
||||
newY = event.clientY - state.lastTop;
|
||||
if (onBackgroundMoved) {
|
||||
onBackgroundMoved(newX + initialLeft, newY + initialTop);
|
||||
}
|
||||
return {
|
||||
left: newX,
|
||||
top: newY,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
backgroundImage,
|
||||
imageWidth,
|
||||
initialLeft = 0,
|
||||
initialTop = 0,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { left, top, zoom } = this.state;
|
||||
|
||||
const finalLeft = initialLeft + left;
|
||||
const finalTop = initialTop + top;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...computeBoxProps({
|
||||
...rest,
|
||||
style: {
|
||||
...rest.style,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
backgroundImage: `url("${backgroundImage}")`,
|
||||
backgroundPosition: `${finalLeft}px ${finalTop}px`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: `${zoom * imageWidth}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
transform: `translate(${finalLeft}px, ${finalTop}px) scale(${zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Stack position="absolute" width="100%">
|
||||
<Stack.Item>
|
||||
<Button icon="minus" onClick={this.handleZoomDecrease} />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow={1}>
|
||||
<ProgressBar
|
||||
minValue={ZOOM_MIN_VAL}
|
||||
value={zoom}
|
||||
maxValue={ZOOM_MAX_VAL}
|
||||
>
|
||||
{zoom}x
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button icon="plus" onClick={this.handleZoomIncrease} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export { Dropdown } from './Dropdown';
|
||||
export { Flex } from './Flex';
|
||||
export { Icon } from './Icon';
|
||||
export { Image } from './Image';
|
||||
export { InfinitePlane } from './InfinitePlane';
|
||||
export { Input } from './Input';
|
||||
export { Knob } from './Knob';
|
||||
export { LabeledControls } from './LabeledControls';
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { toFixed } from 'common/math';
|
||||
|
||||
import { useBackend } from '../backend';
|
||||
import {
|
||||
AnimatedNumber,
|
||||
Box,
|
||||
Button,
|
||||
LabeledList,
|
||||
ProgressBar,
|
||||
Section,
|
||||
} from '../components';
|
||||
import { formatPower } from '../format';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
total_parts: number;
|
||||
max_components: number;
|
||||
total_complexity: number;
|
||||
max_complexity: number;
|
||||
battery_charge: number;
|
||||
battery_max: number;
|
||||
net_power: number;
|
||||
unremovable_circuits: circuit[];
|
||||
removable_circuits: circuit[];
|
||||
};
|
||||
|
||||
type circuit = { name: string; ref: string };
|
||||
|
||||
export const ICAssembly = (props) => {
|
||||
const { data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
total_parts,
|
||||
max_components,
|
||||
total_complexity,
|
||||
max_complexity,
|
||||
battery_charge,
|
||||
battery_max,
|
||||
net_power,
|
||||
unremovable_circuits,
|
||||
removable_circuits,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Window width={600} height={380}>
|
||||
<Window.Content scrollable>
|
||||
<Section title="Status">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Space in Assembly">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
bad: [0.75, 1],
|
||||
}}
|
||||
value={total_parts / max_components}
|
||||
maxValue={1}
|
||||
>
|
||||
{total_parts +
|
||||
' / ' +
|
||||
max_components +
|
||||
' (' +
|
||||
toFixed((total_parts / max_components) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Complexity">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
bad: [0.75, 1],
|
||||
}}
|
||||
value={total_complexity / max_complexity}
|
||||
maxValue={1}
|
||||
>
|
||||
{total_complexity +
|
||||
' / ' +
|
||||
max_complexity +
|
||||
' (' +
|
||||
toFixed((total_complexity / max_complexity) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Cell Charge">
|
||||
{(battery_charge && (
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
bad: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
good: [0.75, 1],
|
||||
}}
|
||||
value={battery_charge / battery_max}
|
||||
maxValue={1}
|
||||
>
|
||||
{battery_charge +
|
||||
' / ' +
|
||||
battery_max +
|
||||
' (' +
|
||||
toFixed((battery_charge / battery_max) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
)) || <Box color="bad">No cell detected.</Box>}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Net Energy">
|
||||
{(net_power === 0 && '0 W/s') || (
|
||||
<AnimatedNumber
|
||||
value={net_power}
|
||||
format={(val) => '-' + formatPower(Math.abs(val)) + '/s'}
|
||||
/>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
{(unremovable_circuits.length && (
|
||||
<ICAssemblyCircuits
|
||||
title="Built-in Components"
|
||||
circuits={unremovable_circuits}
|
||||
/>
|
||||
)) ||
|
||||
null}
|
||||
{(removable_circuits.length && (
|
||||
<ICAssemblyCircuits
|
||||
title="Removable Components"
|
||||
circuits={removable_circuits}
|
||||
/>
|
||||
)) ||
|
||||
null}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const ICAssemblyCircuits = (props: { title: string; circuits: circuit[] }) => {
|
||||
const { act } = useBackend();
|
||||
|
||||
const { title, circuits } = props;
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<LabeledList>
|
||||
{circuits.map((circuit) => (
|
||||
<LabeledList.Item key={circuit.ref} label={circuit.name}>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={() => act('open_circuit', { ref: circuit.ref })}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={() => act('rename_circuit', { ref: circuit.ref })}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={() => act('scan_circuit', { ref: circuit.ref })}
|
||||
>
|
||||
Debugger Scan
|
||||
</Button>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={() => act('remove_circuit', { ref: circuit.ref })}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={() => act('bottom_circuit', { ref: circuit.ref })}
|
||||
>
|
||||
Move to Bottom
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
))}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
318
tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx
Normal file
318
tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { decodeHtmlEntities } from 'common/string';
|
||||
import { Component } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box } from 'tgui/components';
|
||||
import { BoxProps } from 'tgui/components/Box';
|
||||
import { Button, Icon, Stack } from 'tgui-core/components';
|
||||
import { shallowDiffers } from 'tgui-core/react';
|
||||
|
||||
import { Port, PortProps } from './Port';
|
||||
import { CircuitData, PortTypesToColor as PORT_TYPES_TO_COLOR } from './types';
|
||||
|
||||
export type CircuitProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
circuit: CircuitData;
|
||||
color?: string;
|
||||
gridMode?: boolean;
|
||||
onComponentMoved?: (newPos: { x: number; y: number }) => void;
|
||||
} & BoxProps &
|
||||
Pick<
|
||||
PortProps,
|
||||
| 'onPortUpdated'
|
||||
| 'onPortLoaded'
|
||||
| 'onPortMouseDown'
|
||||
| 'onPortMouseUp'
|
||||
| 'onPortRightClick'
|
||||
>;
|
||||
|
||||
export type CircuitState = {
|
||||
lastMousePos: { x: number; y: number } | null;
|
||||
isDragging: boolean;
|
||||
dragPos: { x: number; y: number } | null;
|
||||
startPos: { x: number; y: number } | null;
|
||||
};
|
||||
|
||||
// This has to be a class component to manage window state unfortunately
|
||||
export class CircuitComponent extends Component<CircuitProps, CircuitState> {
|
||||
constructor(props: CircuitProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDragging: false,
|
||||
dragPos: null,
|
||||
startPos: null,
|
||||
lastMousePos: null,
|
||||
};
|
||||
}
|
||||
|
||||
// THIS IS IMPORTANT:
|
||||
// This reduces the amount of unnecessary updates, which reduces the amount of work that has to be done
|
||||
// by the `Plane` component to keep track of where ports are located.
|
||||
shouldComponentUpdate = (nextProps, nextState) => {
|
||||
const { inputs, outputs, activators } = this.props.circuit;
|
||||
|
||||
return (
|
||||
shallowDiffers(this.props, nextProps) ||
|
||||
shallowDiffers(this.state, nextState) ||
|
||||
shallowDiffers(inputs, nextProps.inputs) ||
|
||||
shallowDiffers(outputs, nextProps.outputs) ||
|
||||
shallowDiffers(activators, nextProps.activators)
|
||||
);
|
||||
};
|
||||
|
||||
handleStartDrag = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { x, y } = this.props;
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
lastMousePos: null,
|
||||
isDragging: true,
|
||||
dragPos: { x, y },
|
||||
startPos: { x, y },
|
||||
});
|
||||
window.addEventListener('mousemove', this.handleDrag);
|
||||
window.addEventListener('mouseup', this.handleStopDrag);
|
||||
};
|
||||
|
||||
handleStopDrag = (e: React.MouseEvent<HTMLDivElement> | MouseEvent) => {
|
||||
const { onComponentMoved } = this.props;
|
||||
const { dragPos } = this.state;
|
||||
|
||||
if (dragPos && onComponentMoved) {
|
||||
onComponentMoved({
|
||||
x: this.roundToGrid(dragPos.x),
|
||||
y: this.roundToGrid(dragPos.y),
|
||||
});
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', this.handleDrag);
|
||||
window.removeEventListener('mouseup', this.handleStopDrag);
|
||||
this.setState({ isDragging: false });
|
||||
};
|
||||
|
||||
handleDrag = (e: any) => {
|
||||
const { dragPos, isDragging, lastMousePos } = this.state;
|
||||
if (!dragPos || !isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const { screenZoomX, screenZoomY, screenX, screenY } = e;
|
||||
let xPos = screenZoomX || screenX;
|
||||
let yPos = screenZoomY || screenY;
|
||||
|
||||
if (lastMousePos) {
|
||||
this.setState({
|
||||
dragPos: {
|
||||
x: dragPos.x - (lastMousePos.x - xPos),
|
||||
y: dragPos.y - (lastMousePos.y - yPos),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
lastMousePos: { x: xPos, y: yPos },
|
||||
});
|
||||
};
|
||||
|
||||
// Round the units to the grid (bypass if grid mode is off)
|
||||
roundToGrid = (input_value) => {
|
||||
if (!this.props.gridMode) return input_value;
|
||||
return Math.round(input_value / 10) * 10;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
circuit,
|
||||
color = 'blue',
|
||||
onPortUpdated,
|
||||
onPortLoaded,
|
||||
onPortMouseDown,
|
||||
onPortMouseUp,
|
||||
onPortRightClick,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const { name, ref, inputs, outputs, activators } = circuit;
|
||||
|
||||
const { startPos, dragPos } = this.state;
|
||||
|
||||
const { act } = useBackend();
|
||||
|
||||
let [x_pos, y_pos] = [x, y];
|
||||
if (dragPos && startPos && startPos.x === x_pos && startPos.y === y_pos) {
|
||||
x_pos = this.roundToGrid(dragPos.x);
|
||||
y_pos = this.roundToGrid(dragPos.y);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
left={x_pos + 'px'}
|
||||
top={y_pos + 'px'}
|
||||
onMouseDown={this.handleStartDrag}
|
||||
onMouseUp={this.handleStopDrag}
|
||||
{...rest}
|
||||
>
|
||||
<Box
|
||||
backgroundColor={color}
|
||||
py={1}
|
||||
px={1}
|
||||
className="ObjectComponent__Titlebar"
|
||||
>
|
||||
<Stack align="center" justify="space-between">
|
||||
<Stack.Item>{name}</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="external-link-alt"
|
||||
color="transparent"
|
||||
compact
|
||||
tooltip="View Component UI"
|
||||
tooltipPosition="bottom-end"
|
||||
onClick={() => act('open_circuit', { ref })}
|
||||
/>
|
||||
<Button
|
||||
icon="info"
|
||||
color="transparent"
|
||||
compact
|
||||
tooltip={
|
||||
<Box>
|
||||
<Box mb={1}>{circuit.extended_desc}</Box>
|
||||
{!!circuit.power_draw_idle && (
|
||||
<Box
|
||||
backgroundColor="orange"
|
||||
style={{ borderRadius: '4px' }}
|
||||
px={1}
|
||||
>
|
||||
<Icon name="bolt" mr={1} />
|
||||
Power Draw (Passive): {circuit.power_draw_idle}
|
||||
</Box>
|
||||
)}
|
||||
{!!circuit.power_draw_per_use && (
|
||||
<Box
|
||||
backgroundColor="orange"
|
||||
style={{ borderRadius: '4px' }}
|
||||
px={1}
|
||||
>
|
||||
<Icon name="bolt" mr={1} />
|
||||
Power Draw (Active): {circuit.power_draw_per_use}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
tooltipPosition="bottom-end"
|
||||
/>
|
||||
{!!circuit.removable && (
|
||||
<Button
|
||||
icon="times"
|
||||
color="transparent"
|
||||
compact
|
||||
tooltip="Remove Circuit"
|
||||
tooltipPosition="bottom-end"
|
||||
onClick={() => act('remove_circuit', { ref })}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box className="ObjectComponent__Content" p={1}>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Stack align="flex-start" justify="space-between">
|
||||
<Stack.Item>
|
||||
<Stack vertical fill>
|
||||
{inputs.map((port) => (
|
||||
<Stack.Item key={port.ref}>
|
||||
<Port
|
||||
color={
|
||||
PORT_TYPES_TO_COLOR[decodeHtmlEntities(port.type)]
|
||||
}
|
||||
port={port}
|
||||
onPortUpdated={onPortUpdated}
|
||||
onPortLoaded={onPortLoaded}
|
||||
onPortMouseDown={onPortMouseDown}
|
||||
onPortMouseUp={onPortMouseUp}
|
||||
onPortRightClick={onPortRightClick}
|
||||
/>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item ml={5}>
|
||||
<Stack vertical>
|
||||
{outputs.map((port) => (
|
||||
<Stack.Item key={port.ref}>
|
||||
<Port
|
||||
color={
|
||||
PORT_TYPES_TO_COLOR[decodeHtmlEntities(port.type)]
|
||||
}
|
||||
output
|
||||
port={port}
|
||||
onPortUpdated={onPortUpdated}
|
||||
onPortLoaded={onPortLoaded}
|
||||
onPortMouseDown={onPortMouseDown}
|
||||
onPortMouseUp={onPortMouseUp}
|
||||
onPortRightClick={onPortRightClick}
|
||||
/>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item mt={inputs.length || outputs.length ? 5 : 0}>
|
||||
<Stack align="flex-start" justify="space-between">
|
||||
<Stack.Item>
|
||||
<Stack vertical>
|
||||
{activators
|
||||
.filter((port) => !port.rawdata)
|
||||
.map((port) => (
|
||||
<Stack.Item key={port.ref}>
|
||||
<Port
|
||||
color={
|
||||
PORT_TYPES_TO_COLOR[decodeHtmlEntities(port.type)]
|
||||
}
|
||||
output={!!port.rawdata}
|
||||
port={port}
|
||||
onPortUpdated={onPortUpdated}
|
||||
onPortLoaded={onPortLoaded}
|
||||
onPortMouseDown={onPortMouseDown}
|
||||
onPortMouseUp={onPortMouseUp}
|
||||
onPortRightClick={onPortRightClick}
|
||||
/>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack vertical>
|
||||
{activators
|
||||
.filter((port) => !!port.rawdata)
|
||||
.map((port) => (
|
||||
<Stack.Item key={port.ref}>
|
||||
<Port
|
||||
color={
|
||||
PORT_TYPES_TO_COLOR[decodeHtmlEntities(port.type)]
|
||||
}
|
||||
output={!!port.rawdata}
|
||||
port={port}
|
||||
onPortUpdated={onPortUpdated}
|
||||
onPortLoaded={onPortLoaded}
|
||||
onPortMouseDown={onPortMouseDown}
|
||||
onPortMouseUp={onPortMouseUp}
|
||||
onPortRightClick={onPortRightClick}
|
||||
/>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
299
tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx
Normal file
299
tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { decodeHtmlEntities } from 'common/string';
|
||||
import { Component } from 'react';
|
||||
import { resolveAsset } from 'tgui/assets';
|
||||
import { useBackend, useSharedState } from 'tgui/backend';
|
||||
// TODO: Replace when tgui-core is fixed https://github.com/tgstation/tgui-core/issues/25
|
||||
import { InfinitePlane } from 'tgui/components';
|
||||
|
||||
import { Connection, Connections } from '../common/Connections';
|
||||
import { CircuitComponent } from './CircuitComponent';
|
||||
import { PortProps } from './Port';
|
||||
import {
|
||||
ABSOLUTE_Y_OFFSET,
|
||||
CircuitData,
|
||||
Data,
|
||||
MOUSE_BUTTON_LEFT,
|
||||
PortData,
|
||||
PortTypesToColor,
|
||||
TIME_UNTIL_PORT_RELEASE_WORKS,
|
||||
} from './types';
|
||||
|
||||
export type PlaneProps = {};
|
||||
|
||||
type PlaneState = {
|
||||
locations: Record<string, { x: number; y: number }>;
|
||||
selectedPort: PortData | null;
|
||||
timeUntilPortReleaseTimesOut: number;
|
||||
backgroundX: number;
|
||||
backgroundY: number;
|
||||
zoom: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export class Plane extends Component<PlaneProps, PlaneState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
locations: {},
|
||||
selectedPort: null,
|
||||
timeUntilPortReleaseTimesOut: 0,
|
||||
backgroundX: 50,
|
||||
backgroundY: 50,
|
||||
zoom: 1,
|
||||
mouseX: 0,
|
||||
mouseY: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getPosition = (dom: HTMLElement | null) => {
|
||||
let xPos = 0;
|
||||
let yPos = 0;
|
||||
|
||||
while (dom) {
|
||||
xPos += dom.offsetLeft;
|
||||
yPos += dom.offsetTop;
|
||||
dom = dom.offsetParent as HTMLElement;
|
||||
}
|
||||
|
||||
return {
|
||||
x: xPos,
|
||||
y: yPos + ABSOLUTE_Y_OFFSET,
|
||||
};
|
||||
};
|
||||
|
||||
handlePortLocation = (port: PortData, dom: HTMLSpanElement) => {
|
||||
const { locations } = this.state;
|
||||
|
||||
if (!dom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPosition = locations[port.ref];
|
||||
const position = this.getPosition(dom);
|
||||
|
||||
if (
|
||||
isNaN(position.x) ||
|
||||
isNaN(position.y) ||
|
||||
(lastPosition &&
|
||||
lastPosition.x === position.x &&
|
||||
lastPosition.y === position.y)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
locations[port.ref] = position;
|
||||
|
||||
this.setState({ locations });
|
||||
};
|
||||
|
||||
handleZoomChange = (newZoom) => {
|
||||
this.setState({
|
||||
zoom: newZoom,
|
||||
});
|
||||
};
|
||||
|
||||
handleBackgroundMoved = (newX, newY) => {
|
||||
this.setState({
|
||||
backgroundX: newX,
|
||||
backgroundY: newY,
|
||||
});
|
||||
};
|
||||
|
||||
handleDragging = (event: MouseEvent) => {
|
||||
this.setState((state) => ({
|
||||
mouseX: event.clientX - state.backgroundX,
|
||||
mouseY: event.clientY - state.backgroundY,
|
||||
}));
|
||||
};
|
||||
|
||||
handlePortUp = (port: PortData, ref: HTMLDivElement, event: MouseEvent) => {
|
||||
const { act } = useBackend();
|
||||
const { selectedPort } = this.state;
|
||||
|
||||
if (!selectedPort || selectedPort.ref === port.ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedPort: null,
|
||||
});
|
||||
|
||||
act('wire_internal', { pin1: selectedPort.ref, pin2: port.ref });
|
||||
};
|
||||
|
||||
handlePortRelease = (event: MouseEvent) => {
|
||||
window.removeEventListener('mouseup', this.handlePortRelease);
|
||||
|
||||
// This will let players release their mouse when dragging
|
||||
// to stop connecting the port, whilst letting players
|
||||
// click on the port to click and connect.
|
||||
if (this.state.timeUntilPortReleaseTimesOut > Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedPort: null,
|
||||
});
|
||||
|
||||
window.removeEventListener('mousemove', this.handleDragging);
|
||||
};
|
||||
|
||||
handlePortClick = (
|
||||
port: PortData,
|
||||
ref: HTMLDivElement,
|
||||
event: MouseEvent,
|
||||
) => {
|
||||
if (this.state.selectedPort) {
|
||||
this.handlePortUp(port, ref, event);
|
||||
}
|
||||
|
||||
if (event.button !== MOUSE_BUTTON_LEFT) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
this.setState({
|
||||
selectedPort: port,
|
||||
});
|
||||
|
||||
this.handleDragging(event);
|
||||
|
||||
this.setState({
|
||||
timeUntilPortReleaseTimesOut: Date.now() + TIME_UNTIL_PORT_RELEASE_WORKS,
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', this.handleDragging);
|
||||
window.addEventListener('mouseup', this.handlePortRelease);
|
||||
};
|
||||
|
||||
handlePortRightClick = (
|
||||
port: PortData,
|
||||
ref: HTMLDivElement,
|
||||
event: MouseEvent,
|
||||
) => {
|
||||
const { act } = useBackend();
|
||||
|
||||
event.preventDefault();
|
||||
act('remove_all_wires', {
|
||||
pin: port.ref,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { locations, selectedPort, mouseX, mouseY } = this.state;
|
||||
|
||||
const connections: Connection[] = [];
|
||||
|
||||
for (const circuit of data.circuits) {
|
||||
for (const input of circuit.inputs) {
|
||||
for (const output of input.linked) {
|
||||
const output_port = locations[output.ref];
|
||||
connections.push({
|
||||
color: PortTypesToColor[decodeHtmlEntities(input.type)] || 'blue',
|
||||
from: output_port,
|
||||
to: locations[input.ref],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const activator of circuit.activators) {
|
||||
if (activator.rawdata) {
|
||||
// input
|
||||
for (const output of activator.linked) {
|
||||
const output_port = locations[output.ref];
|
||||
connections.push({
|
||||
color:
|
||||
PortTypesToColor[decodeHtmlEntities(activator.type)] || 'blue',
|
||||
to: output_port,
|
||||
from: locations[activator.ref],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedPort) {
|
||||
const { zoom } = this.state;
|
||||
const portLocation = locations[selectedPort.ref];
|
||||
const mouseCoords = {
|
||||
x: mouseX * Math.pow(zoom, -1),
|
||||
y: (mouseY + ABSOLUTE_Y_OFFSET) * Math.pow(zoom, -1),
|
||||
};
|
||||
connections.push({
|
||||
color:
|
||||
PortTypesToColor[decodeHtmlEntities(selectedPort.type)] || 'blue',
|
||||
from: portLocation,
|
||||
to: mouseCoords,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<InfinitePlane
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundImage={resolveAsset('grid_background.png')}
|
||||
imageWidth={900}
|
||||
onZoomChange={this.handleZoomChange}
|
||||
onBackgroundMoved={this.handleBackgroundMoved}
|
||||
initialLeft={50}
|
||||
initialTop={50}
|
||||
>
|
||||
{data.circuits.map((circuit) => (
|
||||
<Circuit
|
||||
circuit={circuit}
|
||||
key={circuit.ref}
|
||||
onPortLoaded={this.handlePortLocation}
|
||||
onPortUpdated={this.handlePortLocation}
|
||||
onPortMouseDown={this.handlePortClick}
|
||||
onPortMouseUp={this.handlePortUp}
|
||||
onPortRightClick={this.handlePortRightClick}
|
||||
/>
|
||||
))}
|
||||
<Connections connections={connections} />
|
||||
</InfinitePlane>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Circuit = (
|
||||
props: { circuit: CircuitData } & Pick<
|
||||
PortProps,
|
||||
| 'onPortUpdated'
|
||||
| 'onPortLoaded'
|
||||
| 'onPortMouseDown'
|
||||
| 'onPortMouseUp'
|
||||
| 'onPortRightClick'
|
||||
>,
|
||||
) => {
|
||||
const {
|
||||
circuit,
|
||||
onPortUpdated,
|
||||
onPortLoaded,
|
||||
onPortMouseDown,
|
||||
onPortMouseUp,
|
||||
onPortRightClick,
|
||||
} = props;
|
||||
|
||||
const [pos, setPos] = useSharedState('component-pos-' + circuit.ref, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<CircuitComponent
|
||||
circuit={circuit}
|
||||
gridMode
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
onComponentMoved={(val) => setPos(val)}
|
||||
onPortUpdated={onPortUpdated}
|
||||
onPortLoaded={onPortLoaded}
|
||||
onPortMouseDown={onPortMouseDown}
|
||||
onPortMouseUp={onPortMouseUp}
|
||||
onPortRightClick={onPortRightClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
117
tgui/packages/tgui/interfaces/ICAssembly/Port.tsx
Normal file
117
tgui/packages/tgui/interfaces/ICAssembly/Port.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { decodeHtmlEntities } from 'common/string';
|
||||
import { Component, createRef } from 'react';
|
||||
import { Box, Stack, Tooltip } from 'tgui-core/components';
|
||||
|
||||
import { PortData } from './types';
|
||||
|
||||
export type PortProps = {
|
||||
port: PortData;
|
||||
onPortUpdated?: (port: PortData, iconRef: HTMLSpanElement | null) => void;
|
||||
onPortLoaded?: (port: PortData, iconRef: HTMLSpanElement | null) => void;
|
||||
onPortMouseDown?: (
|
||||
port: PortData,
|
||||
iconRef: HTMLSpanElement | null,
|
||||
e: MouseEvent,
|
||||
) => void;
|
||||
onPortMouseUp?: (
|
||||
port: PortData,
|
||||
iconRef: HTMLSpanElement | null,
|
||||
e: MouseEvent,
|
||||
) => void;
|
||||
onPortRightClick?: (
|
||||
port: PortData,
|
||||
iconRef: HTMLSpanElement | null,
|
||||
e: MouseEvent,
|
||||
) => void;
|
||||
color?: string;
|
||||
output?: boolean;
|
||||
};
|
||||
|
||||
export type PortState = {
|
||||
portRef: React.RefObject<HTMLSpanElement>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export class Port extends Component<PortProps, PortState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
portRef: createRef(),
|
||||
};
|
||||
}
|
||||
|
||||
handlePortMouseDown = (e) => {
|
||||
const { port, onPortMouseDown = () => {} } = this.props;
|
||||
onPortMouseDown(port, this.state.portRef.current, e);
|
||||
};
|
||||
|
||||
handlePortMouseUp = (e) => {
|
||||
const { port, onPortMouseUp = () => {} } = this.props;
|
||||
onPortMouseUp(port, this.state.portRef.current, e);
|
||||
};
|
||||
|
||||
handlePortRightClick = (e) => {
|
||||
const { port, onPortRightClick = () => {} } = this.props;
|
||||
onPortRightClick(port, this.state.portRef.current, e);
|
||||
};
|
||||
|
||||
componentDidUpdate = () => {
|
||||
const { port, onPortUpdated } = this.props;
|
||||
if (onPortUpdated) {
|
||||
onPortUpdated(port, this.state.portRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
const { port, onPortLoaded } = this.props;
|
||||
if (onPortLoaded) {
|
||||
onPortLoaded(port, this.state.portRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
renderDisplayName = () => {
|
||||
const { port } = this.props;
|
||||
|
||||
return <Stack.Item>{port.name}</Stack.Item>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { portRef: iconRef } = this.state;
|
||||
const { port, color, output } = this.props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={decodeHtmlEntities(port.type)}
|
||||
position={'bottom-start'}
|
||||
>
|
||||
<Stack align="center" justify={output ? 'flex-end' : 'flex-start'}>
|
||||
{!!output && this.renderDisplayName()}
|
||||
<Stack.Item>
|
||||
<Box
|
||||
className="ObjectComponent__Port"
|
||||
onMouseDown={this.handlePortMouseDown}
|
||||
onContextMenu={this.handlePortRightClick}
|
||||
onMouseUp={this.handlePortMouseUp}
|
||||
textAlign="center"
|
||||
>
|
||||
<svg
|
||||
style={{ width: '100%', height: '100%', position: 'absolute' }}
|
||||
viewBox="0, 0, 100, 100"
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="50"
|
||||
className={`color-fill-${color}`}
|
||||
/>
|
||||
</svg>
|
||||
<span ref={iconRef} className="ObjectComponent__PortPos" />
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
{!output && this.renderDisplayName()}
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
161
tgui/packages/tgui/interfaces/ICAssembly/index.tsx
Normal file
161
tgui/packages/tgui/interfaces/ICAssembly/index.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { toFixed } from 'common/math';
|
||||
import { useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { LabeledList } from 'tgui/components';
|
||||
import { formatPower } from 'tgui/format';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import {
|
||||
AnimatedNumber,
|
||||
Box,
|
||||
Button,
|
||||
ProgressBar,
|
||||
Section,
|
||||
} from 'tgui-core/components';
|
||||
|
||||
import { Plane } from './Plane';
|
||||
import { Data } from './types';
|
||||
|
||||
export const ICAssembly = (props) => {
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const { act } = useBackend();
|
||||
return (
|
||||
<Window
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
color="transparent"
|
||||
width={2.5}
|
||||
height={2}
|
||||
textAlign="center"
|
||||
icon="pencil"
|
||||
tooltip="Edit Name"
|
||||
tooltipPosition="bottom-start"
|
||||
onClick={() => act('rename')}
|
||||
/>
|
||||
<Button
|
||||
color="transparent"
|
||||
width={2.5}
|
||||
height={2}
|
||||
textAlign="center"
|
||||
icon="info"
|
||||
tooltip="Circuit Info"
|
||||
tooltipPosition="bottom-start"
|
||||
selected={showInfo}
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
width={1280}
|
||||
height={800}
|
||||
>
|
||||
<Window.Content style={{ background: 'none' }}>
|
||||
<Plane />
|
||||
{showInfo && <CircuitInfo />}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const CircuitInfo = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
total_parts,
|
||||
max_components,
|
||||
total_complexity,
|
||||
max_complexity,
|
||||
battery_charge,
|
||||
battery_max,
|
||||
net_power,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Section
|
||||
position="absolute"
|
||||
top={4}
|
||||
right={2}
|
||||
backgroundColor="rgba(0, 0, 0, 0.7)"
|
||||
style={{ borderRadius: '16px' }}
|
||||
width={40}
|
||||
p={2}
|
||||
title="Circuit Info"
|
||||
>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Space in Assembly">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
bad: [0.75, 1],
|
||||
}}
|
||||
value={total_parts / max_components}
|
||||
maxValue={1}
|
||||
>
|
||||
{total_parts +
|
||||
' / ' +
|
||||
max_components +
|
||||
' (' +
|
||||
toFixed((total_parts / max_components) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Complexity">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
bad: [0.75, 1],
|
||||
}}
|
||||
value={total_complexity / max_complexity}
|
||||
maxValue={1}
|
||||
>
|
||||
{total_complexity +
|
||||
' / ' +
|
||||
max_complexity +
|
||||
' (' +
|
||||
toFixed((total_complexity / max_complexity) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Cell Charge"
|
||||
buttons={
|
||||
<Button
|
||||
icon="eject"
|
||||
onClick={() => act('remove_cell')}
|
||||
tooltip="Remove Cell"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(battery_charge && (
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
bad: [0, 0.25],
|
||||
average: [0.5, 0.75],
|
||||
good: [0.75, 1],
|
||||
}}
|
||||
value={battery_charge / battery_max}
|
||||
maxValue={1}
|
||||
>
|
||||
{battery_charge +
|
||||
' / ' +
|
||||
battery_max +
|
||||
' (' +
|
||||
toFixed((battery_charge / battery_max) * 100, 1) +
|
||||
'%)'}
|
||||
</ProgressBar>
|
||||
)) || <Box color="bad">No cell detected.</Box>}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Net Energy">
|
||||
{(net_power === 0 && '0 W/s') || (
|
||||
<AnimatedNumber
|
||||
value={net_power}
|
||||
format={(val) => '-' + formatPower(Math.abs(val)) + '/s'}
|
||||
/>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
70
tgui/packages/tgui/interfaces/ICAssembly/types.ts
Normal file
70
tgui/packages/tgui/interfaces/ICAssembly/types.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export type Data = {
|
||||
total_parts: number;
|
||||
max_components: number;
|
||||
total_complexity: number;
|
||||
max_complexity: number;
|
||||
battery_charge: number;
|
||||
battery_max: number;
|
||||
net_power: number;
|
||||
circuits: CircuitData[];
|
||||
};
|
||||
|
||||
export type CircuitData = {
|
||||
name: string;
|
||||
desc: string;
|
||||
ref: string;
|
||||
removable: boolean;
|
||||
complexity: number;
|
||||
power_draw_idle: number;
|
||||
power_draw_per_use: number;
|
||||
extended_desc: string;
|
||||
inputs: PortData[];
|
||||
outputs: PortData[];
|
||||
activators: PortData[];
|
||||
};
|
||||
|
||||
export type PortData = {
|
||||
type: string;
|
||||
name: string;
|
||||
data: string;
|
||||
rawdata: string;
|
||||
ref: string;
|
||||
linked: LinkedPortData[];
|
||||
};
|
||||
|
||||
export type LinkedPortData = {
|
||||
ref: string;
|
||||
name: string;
|
||||
holder_ref: string;
|
||||
holder_name: string;
|
||||
};
|
||||
|
||||
export enum PortTypes {
|
||||
IC_FORMAT_ANY = '<ANY>',
|
||||
IC_FORMAT_STRING = '<TEXT>',
|
||||
IC_FORMAT_CHAR = '<CHAR>',
|
||||
IC_FORMAT_COLOR = '<COLOR>',
|
||||
IC_FORMAT_NUMBER = '<NUM>',
|
||||
IC_FORMAT_DIR = '<DIR>',
|
||||
IC_FORMAT_BOOLEAN = '<BOOL>',
|
||||
IC_FORMAT_REF = '<REF>',
|
||||
IC_FORMAT_LIST = '<LIST>',
|
||||
IC_FORMAT_PULSE = '<PULSE>',
|
||||
}
|
||||
|
||||
export const PortTypesToColor = {
|
||||
[PortTypes.IC_FORMAT_ANY]: 'olive',
|
||||
[PortTypes.IC_FORMAT_STRING]: 'green',
|
||||
[PortTypes.IC_FORMAT_CHAR]: 'darkyellow',
|
||||
[PortTypes.IC_FORMAT_COLOR]: 'pink',
|
||||
[PortTypes.IC_FORMAT_NUMBER]: 'blue',
|
||||
[PortTypes.IC_FORMAT_DIR]: 'darkred',
|
||||
[PortTypes.IC_FORMAT_BOOLEAN]: 'red',
|
||||
[PortTypes.IC_FORMAT_REF]: 'darkblue',
|
||||
[PortTypes.IC_FORMAT_LIST]: 'darkgreen',
|
||||
[PortTypes.IC_FORMAT_PULSE]: 'yellow',
|
||||
};
|
||||
|
||||
export const ABSOLUTE_Y_OFFSET = -32;
|
||||
export const MOUSE_BUTTON_LEFT = 0;
|
||||
export const TIME_UNTIL_PORT_RELEASE_WORKS = 100;
|
||||
95
tgui/packages/tgui/interfaces/common/Connections.tsx
Normal file
95
tgui/packages/tgui/interfaces/common/Connections.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { classes } from 'common/react';
|
||||
import { CSS_COLORS } from 'tgui/constants';
|
||||
|
||||
const SVG_CURVE_INTENSITY = 64;
|
||||
|
||||
enum ConnectionStyle {
|
||||
CURVE = 'curve',
|
||||
SUBWAY = 'subway',
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Connection = {
|
||||
// X, Y starting point
|
||||
from: Position;
|
||||
// X, Y ending point
|
||||
to: Position;
|
||||
// Color of the line, defaults to blue
|
||||
color?: string;
|
||||
// Type of line - Curvy or Straight / angled, defaults to curvy
|
||||
style?: ConnectionStyle;
|
||||
// Optional: the ref of what element this connection is sourced
|
||||
ref?: string;
|
||||
};
|
||||
|
||||
export const Connections = (props: {
|
||||
connections: Connection[];
|
||||
zLayer?: number;
|
||||
lineWidth?: number;
|
||||
}) => {
|
||||
const { connections, zLayer = -1, lineWidth = '2px' } = props;
|
||||
|
||||
const isColorClass = (str) => {
|
||||
if (typeof str === 'string') {
|
||||
return CSS_COLORS.includes(str as any);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
zIndex: zLayer,
|
||||
}}
|
||||
>
|
||||
{connections.map((val, index) => {
|
||||
const from = val.from;
|
||||
const to = val.to;
|
||||
if (!to || !from) {
|
||||
return;
|
||||
}
|
||||
|
||||
val.color = val.color || 'blue';
|
||||
val.style = val.style || ConnectionStyle.CURVE;
|
||||
|
||||
// Starting point
|
||||
let path = `M ${from.x} ${from.y}`;
|
||||
|
||||
switch (val.style) {
|
||||
case ConnectionStyle.CURVE: {
|
||||
path += `C ${from.x + SVG_CURVE_INTENSITY}, ${from.y},`;
|
||||
path += `${to.x - SVG_CURVE_INTENSITY}, ${to.y},`;
|
||||
path += `${to.x}, ${to.y}`;
|
||||
break;
|
||||
}
|
||||
case ConnectionStyle.SUBWAY: {
|
||||
const yDiff = Math.abs(from.y - (to.y - 16));
|
||||
path += `L ${to.x - yDiff} ${from.y}`;
|
||||
path += `L ${to.x - 16} ${to.y}`;
|
||||
path += `L ${to.x} ${to.y}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<path
|
||||
className={classes([
|
||||
isColorClass(val.color) && `color-stroke-${val.color}`,
|
||||
])}
|
||||
key={index}
|
||||
d={path}
|
||||
fill="transparent"
|
||||
stroke-width={lineWidth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,16 @@
|
||||
@use '../colors.scss';
|
||||
@use '../base.scss';
|
||||
|
||||
$bg-map: colors.$bg-map !default;
|
||||
$fg-map: colors.$fg-map !default;
|
||||
|
||||
.CircuitInfo__Examined {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
padding: 8px;
|
||||
border-radius: 5px;
|
||||
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ObjectComponent__Titlebar {
|
||||
border-top-left-radius: 12px;
|
||||
@@ -21,6 +30,20 @@ $bg-map: colors.$bg-map !default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ObjectComponent__Greyed_Content {
|
||||
white-space: nowrap;
|
||||
background-color: rgba(19, 19, 19, 0.5);
|
||||
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ObjectComponent__Port {
|
||||
position: relative;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.ObjectComponent__PortPos {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -29,8 +52,21 @@ $bg-map: colors.$bg-map !default;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@each $color-name, $color-value in $bg-map {
|
||||
@each $color-name, $color-value in $fg-map {
|
||||
.color-stroke-#{$color-name} {
|
||||
stroke: $color-value !important;
|
||||
}
|
||||
|
||||
.color-fill-#{$color-name} {
|
||||
fill: $color-value !important;
|
||||
}
|
||||
}
|
||||
|
||||
$border-color: #88bfff !default;
|
||||
$border-radius: base.$border-radius !default;
|
||||
|
||||
.IntegratedCircuit__BlueBorder {
|
||||
border: base.em(1px) solid $border-color;
|
||||
border: base.em(1px) solid rgba($border-color, 0.75);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
@@ -2037,6 +2037,7 @@
|
||||
#include "code\modules\asset_cache\asset_list.dm"
|
||||
#include "code\modules\asset_cache\asset_list_items.dm"
|
||||
#include "code\modules\asset_cache\assets\chat.dm"
|
||||
#include "code\modules\asset_cache\assets\circuits.dm"
|
||||
#include "code\modules\asset_cache\assets\fontawesome.dm"
|
||||
#include "code\modules\asset_cache\assets\icon_ref_map.dm"
|
||||
#include "code\modules\asset_cache\assets\jquery.dm"
|
||||
|
||||
Reference in New Issue
Block a user