From c0228d937bdef7d05e1ef4a213f2265a7ee1e626 Mon Sep 17 00:00:00 2001 From: CHOMPStation2 <58959929+CHOMPStation2@users.noreply.github.com> Date: Sun, 6 Oct 2024 07:18:41 -0700 Subject: [PATCH] [MIRROR] Switch circuits to a grid visual coding system (#9173) Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: CHOMPStation2 --- code/modules/asset_cache/assets/circuits.dm | 4 + .../integrated_electronics/core/assemblies.dm | 93 ++--- .../core/integrated_circuit.dm | 18 +- icons/UI_Icons/tgui/grid_background.png | Bin 0 -> 21650 bytes .../tgui/components/InfinitePlane.tsx | 222 ++++++++++++ tgui/packages/tgui/components/index.ts | 1 + tgui/packages/tgui/interfaces/ICAssembly.tsx | 179 ---------- .../ICAssembly/CircuitComponent.tsx | 318 ++++++++++++++++++ .../tgui/interfaces/ICAssembly/Plane.tsx | 299 ++++++++++++++++ .../tgui/interfaces/ICAssembly/Port.tsx | 117 +++++++ .../tgui/interfaces/ICAssembly/index.tsx | 161 +++++++++ .../tgui/interfaces/ICAssembly/types.ts | 70 ++++ .../tgui/interfaces/common/Connections.tsx | 95 ++++++ .../styles/interfaces/IntegratedCircuit.scss | 40 ++- vorestation.dme | 1 + 15 files changed, 1379 insertions(+), 239 deletions(-) create mode 100644 code/modules/asset_cache/assets/circuits.dm create mode 100644 icons/UI_Icons/tgui/grid_background.png create mode 100644 tgui/packages/tgui/components/InfinitePlane.tsx delete mode 100644 tgui/packages/tgui/interfaces/ICAssembly.tsx create mode 100644 tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx create mode 100644 tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx create mode 100644 tgui/packages/tgui/interfaces/ICAssembly/Port.tsx create mode 100644 tgui/packages/tgui/interfaces/ICAssembly/index.tsx create mode 100644 tgui/packages/tgui/interfaces/ICAssembly/types.ts create mode 100644 tgui/packages/tgui/interfaces/common/Connections.tsx diff --git a/code/modules/asset_cache/assets/circuits.dm b/code/modules/asset_cache/assets/circuits.dm new file mode 100644 index 0000000000..c783fcf8b2 --- /dev/null +++ b/code/modules/asset_cache/assets/circuits.dm @@ -0,0 +1,4 @@ +/datum/asset/simple/circuit_assets + assets = list( + "grid_background.png" = 'icons/UI_Icons/tgui/grid_background.png', + ) diff --git a/code/modules/integrated_electronics/core/assemblies.dm b/code/modules/integrated_electronics/core/assemblies.dm index 15ffcb6673..2bf148dbf1 100644 --- a/code/modules/integrated_electronics/core/assemblies.dm +++ b/code/modules/integrated_electronics/core/assemblies.dm @@ -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 diff --git a/code/modules/integrated_electronics/core/integrated_circuit.dm b/code/modules/integrated_electronics/core/integrated_circuit.dm index b9e67b7607..ba3dbc709c 100644 --- a/code/modules/integrated_electronics/core/integrated_circuit.dm +++ b/code/modules/integrated_electronics/core/integrated_circuit.dm @@ -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) diff --git a/icons/UI_Icons/tgui/grid_background.png b/icons/UI_Icons/tgui/grid_background.png new file mode 100644 index 0000000000000000000000000000000000000000..228d373456cec0ba541e8f2f652edc9c2334b751 GIT binary patch literal 21650 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 RA4fqTEj4}BVilW^{{x4e&vyU- literal 0 HcmV?d00001 diff --git a/tgui/packages/tgui/components/InfinitePlane.tsx b/tgui/packages/tgui/components/InfinitePlane.tsx new file mode 100644 index 0000000000..ca53e538a1 --- /dev/null +++ b/tgui/packages/tgui/components/InfinitePlane.tsx @@ -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) => { + 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) => { + 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 ( +
+
+
+ {children} +
+ + + +
+ ); + } +} diff --git a/tgui/packages/tgui/components/index.ts b/tgui/packages/tgui/components/index.ts index 95c2dd9260..6c17662086 100644 --- a/tgui/packages/tgui/components/index.ts +++ b/tgui/packages/tgui/components/index.ts @@ -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'; diff --git a/tgui/packages/tgui/interfaces/ICAssembly.tsx b/tgui/packages/tgui/interfaces/ICAssembly.tsx deleted file mode 100644 index 8672bbf175..0000000000 --- a/tgui/packages/tgui/interfaces/ICAssembly.tsx +++ /dev/null @@ -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(); - - const { - total_parts, - max_components, - total_complexity, - max_complexity, - battery_charge, - battery_max, - net_power, - unremovable_circuits, - removable_circuits, - } = data; - - return ( - - -
- - - - {total_parts + - ' / ' + - max_components + - ' (' + - toFixed((total_parts / max_components) * 100, 1) + - '%)'} - - - - - {total_complexity + - ' / ' + - max_complexity + - ' (' + - toFixed((total_complexity / max_complexity) * 100, 1) + - '%)'} - - - - {(battery_charge && ( - - {battery_charge + - ' / ' + - battery_max + - ' (' + - toFixed((battery_charge / battery_max) * 100, 1) + - '%)'} - - )) || No cell detected.} - - - {(net_power === 0 && '0 W/s') || ( - '-' + formatPower(Math.abs(val)) + '/s'} - /> - )} - - -
- {(unremovable_circuits.length && ( - - )) || - null} - {(removable_circuits.length && ( - - )) || - null} -
-
- ); -}; - -const ICAssemblyCircuits = (props: { title: string; circuits: circuit[] }) => { - const { act } = useBackend(); - - const { title, circuits } = props; - - return ( -
- - {circuits.map((circuit) => ( - - - - - - - - ))} - -
- ); -}; diff --git a/tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx b/tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx new file mode 100644 index 0000000000..c5d255e301 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ICAssembly/CircuitComponent.tsx @@ -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 { + 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) => { + 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 | 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 ( + + + + {name} + +