Implements color matrix and transform modification support for Filterrific (#93867)

## About The Pull Request

Allows users to edit color matrix filters and layered filters'
transforms through Filterrific, which were the two last unsupported
filter fields. The menus are pretty minimal, but you shouldn't be
touching them if you don't know what you're doing anyways.


https://github.com/user-attachments/assets/e87447f3-e5d6-4f6c-a8e4-bf50a1d5fa79

## Why It's Good For The Game

Makes our tooling actually work

## Changelog
🆑
admin: Implemented color matrix and transform modification support for
Filterrific
/🆑
This commit is contained in:
SmArtKar
2025-11-13 08:55:15 +01:00
committed by GitHub
parent 422f6400a2
commit 78ea4187fc
5 changed files with 184 additions and 11 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ GLOBAL_PROTECT(href_token)
var/deadmined
var/datum/filter_editor/filteriffic
var/datum/filter_editor/filterrific
var/datum/particle_editor/particle_test
var/datum/colorblind_tester/color_test
var/datum/plane_master_debug/plane_debug
@@ -10,7 +10,7 @@
/datum/filter_editor/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Filteriffic")
ui = new(user, src, "Filterrific")
ui.open()
/datum/filter_editor/ui_static_data(mob/user)
@@ -21,7 +21,14 @@
/datum/filter_editor/ui_data()
var/list/data = list()
data["target_name"] = target.name
data["target_filter_data"] = target.filter_data
var/list/target_filter_data = list()
for (var/list/filter_info as anything in target.filter_data)
filter_info = deep_copy_list(filter_info)
if (filter_info["transform"])
var/matrix/filter_transform = filter_info["transform"]
filter_info["transform"] = list("a" = filter_transform.a, "b" = filter_transform.b, "c" = filter_transform.c, "d" = filter_transform.d, "e" = filter_transform.e, "f" = filter_transform.f)
target_filter_data += list(filter_info)
data["target_filter_data"] = target_filter_data
return data
/datum/filter_editor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -59,7 +66,7 @@
target.modify_filter(params["name"], params["new_data"])
. = TRUE
if("modify_color_value")
var/new_color = input(usr, "Pick new filter color", "Filteriffic Colors!") as color|null
var/new_color = input(usr, "Pick new filter color", "Filterrific Colors!") as color|null
if(new_color)
target.transition_filter(params["name"], list("color" = new_color), 4)
. = TRUE
@@ -68,6 +75,16 @@
if(new_icon)
target.modify_filter(params["name"], list("icon" = new_icon))
. = TRUE
if("modify_transform_value")
var/list/filter_info = target.get_filter_data(params["name"])
if (!filter_info)
return
var/matrix/new_transform = matrix(filter_info[params["field_name"]])
if (!(params["transform_key"] in list("a", "b", "c", "d", "e", "f")))
return
new_transform.vars[params["transform_key"]] = text2num(params["transform_value"])
target.modify_filter(params["name"], list(params["field_name"] = new_transform))
. = TRUE
if("mass_apply")
if(!check_rights_for(usr.client, R_FUN))
to_chat(usr, span_userdanger("Stay in your lane, jannie."))
@@ -4,7 +4,7 @@
*
* More than just a completely true statement, this datum is created as a tgui interface
* allowing you to modify each vector until you know what you're doing.
* Much like filteriffic, 'nobody wants to learn matrix math' is meant for developers like you and I
* Much like filterrific, 'nobody wants to learn matrix math' is meant for developers like you and I
* to implement interesting matrix transformations without the hassle if needing to know... algebra? Damn, i'm stupid.
*/
/datum/nobody_wants_to_learn_matrix_math
+2 -2
View File
@@ -1126,8 +1126,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
/client/proc/open_filter_editor(atom/in_atom)
if(holder)
holder.filteriffic = new /datum/filter_editor(in_atom)
holder.filteriffic.ui_interact(mob)
holder.filterrific = new /datum/filter_editor(in_atom)
holder.filterrific.ui_interact(mob)
///opens the particle editor UI for the in_atom object for this client
/client/proc/open_particle_editor(atom/movable/in_atom)
@@ -11,6 +11,7 @@ import {
NoticeBox,
NumberInput,
Section,
Stack,
} from 'tgui-core/components';
import { numberOfDecimalDigits, toFixed } from 'tgui-core/math';
@@ -193,6 +194,159 @@ const FilterOptionsEntry = (props) => {
);
};
const FilterTransformEntry = (props) => {
const { value, name, filterName } = props;
const { act } = useBackend();
return (
<>
<Stack>
{['a', 'b', 'c'].map((letter_key) => (
<Stack.Item key={letter_key}>
<Box inline ml={2} mr={1}>
{letter_key}:
</Box>
<NumberInput
value={value ? value[letter_key] || 0 : 0}
minValue={letter_key === 'c' ? -480 : -4}
maxValue={letter_key === 'c' ? 480 : 4}
step={letter_key === 'c' ? 1 : 0.01}
stepPixelSize={5}
width="39px"
onChange={(value) =>
act('modify_transform_value', {
name: filterName,
field_name: name,
transform_key: letter_key,
transform_value: value,
})
}
/>
</Stack.Item>
))}
</Stack>
<Stack>
{['d', 'e', 'f'].map((letter_key) => (
<Stack.Item key={letter_key}>
<Box inline ml={2} mr={1}>
{letter_key}:
</Box>
<NumberInput
value={value ? value[letter_key] || 0 : 0}
minValue={letter_key === 'f' ? -480 : -4}
maxValue={letter_key === 'f' ? 480 : 4}
step={letter_key === 'f' ? 1 : 0.01}
stepPixelSize={5}
width="39px"
onChange={(value) =>
act('modify_transform_value', {
name: filterName,
field_name: name,
transform_key: letter_key,
transform_value: value,
})
}
/>
</Stack.Item>
))}
</Stack>
</>
);
};
const FilterMatrixEntry = (props) => {
const { name, value, filterName, filterType } = props;
const { act } = useBackend();
const matrix_sizes = [9, 12, 16, 20];
const resize_matrix = (matrix, size) => {
let identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
switch (size) {
case 12:
identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0];
break;
case 16:
identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
break;
case 20:
identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0];
break;
}
if (matrix === null || matrix === undefined) return identity;
for (let i = 0; i < Math.min(size, matrix.length); i++) {
if (matrix.length === 9)
identity[i + Math.floor(i / 3)] = matrix[i]; // Account for skipped constants
else identity[i] = matrix[i];
}
return identity;
};
let matrix = value;
if (value === null || value === undefined) {
matrix = resize_matrix(value, 9);
}
const processed_matrix = [];
const row_width = matrix.length > 9 ? 4 : 3;
for (let i = 0; i < (matrix.length > 9 ? matrix.length / 4 : 3); i++) {
const new_row = [];
for (let j = 0; j < row_width; j++) {
new_row.push(matrix[i * row_width + j]);
}
processed_matrix.push(new_row);
}
return (
<Box>
<Dropdown
displayText="Matrix Size"
selected={matrix.length}
options={matrix_sizes.map((size) => `${size} elements`)}
onSelected={(option) =>
matrix.length === parseInt(option.split(' '))
? null
: act('modify_filter_value', {
name: filterName,
new_data: {
[name]: resize_matrix(matrix, parseInt(option.split(' '))),
},
})
}
/>
<Stack vertical>
{processed_matrix.map((matrix_row, row_index) => (
<Stack.Item key={row_index}>
<Stack>
{matrix_row.map((matrix_elem, elem_index) => (
<Stack.Item key={elem_index}>
<NumberInput
value={matrix[row_index * row_width + elem_index]}
minValue={-4}
maxValue={4}
step={0.01}
stepPixelSize={5}
width="39px"
onChange={(value) => {
matrix[row_index * row_width + elem_index] = value;
act('transition_filter_value', {
name: filterName,
new_data: {
[name]: matrix,
},
});
}}
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
))}
</Stack>
</Box>
);
};
const FilterDataEntry = (props) => {
const { name, value, hasValue, filterName, filterType } = props;
@@ -204,6 +358,8 @@ const FilterDataEntry = (props) => {
icon: <FilterIconEntry {...props} />,
flags: <FilterFlagsEntry {...props} />,
options: <FilterOptionsEntry {...props} />,
transform: <FilterTransformEntry {...props} />,
matrix: <FilterMatrixEntry {...props} />,
plug: 'Not Implemented',
};
@@ -214,7 +370,7 @@ const FilterDataEntry = (props) => {
render_source: 'string',
flags: 'flags',
size: 'float',
color: { default: 'color', color: 'plug' },
color: { default: 'color', color: 'matrix' },
offset: 'float',
radius: 'int',
falloff: 'float',
@@ -225,7 +381,7 @@ const FilterDataEntry = (props) => {
repeat: 'int',
space: 'options',
blend_mode: 'options',
transform: 'plug',
transform: 'transform',
};
let filterInputType = filterEntryMap[name];
@@ -315,7 +471,7 @@ const FilterEntry = (props) => {
);
};
export const Filteriffic = (props) => {
export const Filterrific = (props) => {
const { act, data } = useBackend();
const name = data.target_name || 'Unknown Object';
const filters = data.target_filter_data || {};
@@ -325,7 +481,7 @@ export const Filteriffic = (props) => {
const [hiddenSecret, setHiddenSecret] = useState(false);
return (
<Window title="Filteriffic" width={500} height={500}>
<Window title="Filterrific" width={500} height={500}>
<Window.Content scrollable>
<NoticeBox danger>
DO NOT MESS WITH EXISTING FILTERS IF YOU DO NOT KNOW THE CONSEQUENCES.