mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-25 04:57:47 +01:00
use gauss elimination for color matrix (#19190)
* use gauss elimination for color matrix * . * Update functions.ts * Update functions.ts * some more improvements * . * . * . * ugh --------- Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
co-authored by
Cameron Lennox
parent
3303d1617b
commit
6c5f04de76
@@ -626,19 +626,19 @@ ADMIN_VERB(cmd_admin_add_freeform_ai_law, R_FUN, "Add Custom AI law", "Adds a cu
|
||||
command_announcement.Announce("Ion storm detected near the [station_name()]. Please check all AI-controlled equipment for errors.", "Anomaly Alert", new_sound = 'sound/AI/ionstorm.ogg')
|
||||
feedback_add_details("admin_verb","IONC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/traget_mob as mob in GLOB.mob_list)
|
||||
if(!traget_mob)
|
||||
ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/target_mob as mob in GLOB.mob_list)
|
||||
if(!target_mob)
|
||||
return
|
||||
if(!istype(traget_mob))
|
||||
if(!istype(target_mob))
|
||||
tgui_alert_async(user, "Cannot revive a ghost")
|
||||
return
|
||||
if(CONFIG_GET(flag/allow_admin_rev))
|
||||
traget_mob.revive()
|
||||
target_mob.revive()
|
||||
|
||||
log_admin("[key_name(user)] healed / revived [key_name(traget_mob)]")
|
||||
var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(traget_mob)]!")
|
||||
log_admin("[key_name(user)] healed / revived [key_name(target_mob)]")
|
||||
var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(target_mob)]!")
|
||||
message_admins(msg)
|
||||
admin_ticket_log(traget_mob, msg)
|
||||
admin_ticket_log(target_mob, msg)
|
||||
else
|
||||
tgui_alert_async(user, "Admin revive disabled")
|
||||
feedback_add_details("admin_verb","REJU") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
sleep(rand(3,5))
|
||||
if(!length(neighbors))
|
||||
break
|
||||
if(QDELETED(src)) // we sleep, might get deleted!
|
||||
return
|
||||
spread_to(pick(neighbors))
|
||||
|
||||
// We shouldn't have spawned if the controller doesn't exist.
|
||||
|
||||
@@ -36,7 +36,7 @@ Controlled by the player_tips subsystem under code/controllers/subsystems/player
|
||||
if(!(target_mob.client?.prefs?.read_preference(/datum/preference/toggle/player_tips)))
|
||||
return
|
||||
if(target_mob.key && !(target_mob.key in HasReceived))
|
||||
to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\n Player tips appear every 45-75 minutes."))
|
||||
to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\nPlayer tips appear every 45-75 minutes."))
|
||||
HasReceived.Add(target_mob.key)
|
||||
to_chat(target_mob, span_notice("[GLOB.is_valid_url.Replace(last_tip, span_linkify("$1"))]"))
|
||||
|
||||
|
||||
@@ -2,13 +2,7 @@ import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Input, Section, Stack } from 'tgui-core/components';
|
||||
import { computeMatrixFromPairs, isValidHex } from '../functions';
|
||||
import { ColorMatrixColorBox } from '../Helpers/MatrixColorBox';
|
||||
import type {
|
||||
ColorPair,
|
||||
ColorUpdate,
|
||||
Data,
|
||||
MatrixColors,
|
||||
SelectedId,
|
||||
} from '../types';
|
||||
import type { ColorPair, ColorUpdate, Data, SelectedId } from '../types';
|
||||
|
||||
export const ColorMateMatrixSolver = (props: {
|
||||
activeID: SelectedId;
|
||||
@@ -51,28 +45,19 @@ export const ColorMateMatrixSolver = (props: {
|
||||
try {
|
||||
const matrix = computeMatrixFromPairs(colorPairs);
|
||||
|
||||
const newMatrixcolors: MatrixColors = {
|
||||
rr: matrix[0][0],
|
||||
rg: matrix[0][1],
|
||||
rb: matrix[0][2],
|
||||
const parts: number[] = [];
|
||||
|
||||
gr: matrix[1][0],
|
||||
gg: matrix[1][1],
|
||||
gb: matrix[1][2],
|
||||
for (let col = 0; col < 3; col++) {
|
||||
for (let row = 0; row < 3; row++) {
|
||||
parts.push(Math.round(matrix[row][col] * 100) / 100);
|
||||
}
|
||||
}
|
||||
|
||||
br: matrix[2][0],
|
||||
bg: matrix[2][1],
|
||||
bb: matrix[2][2],
|
||||
|
||||
cr: matrix[0][3],
|
||||
cg: matrix[1][3],
|
||||
cb: matrix[2][3],
|
||||
};
|
||||
|
||||
const ourMatrix = Object.values(newMatrixcolors)
|
||||
.map((v) => v.toFixed(2))
|
||||
.toString();
|
||||
for (let row = 0; row < 3; row++) {
|
||||
parts.push(Math.round(matrix[row][3] * 100) / 100);
|
||||
}
|
||||
|
||||
const ourMatrix = parts.toString();
|
||||
act('set_matrix_string', { value: ourMatrix });
|
||||
} catch (err) {
|
||||
console.log(`Matrix computation failed: ${err.message}`);
|
||||
@@ -122,7 +107,35 @@ export const ColorMateMatrixSolver = (props: {
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label">{`==>`}</Box>
|
||||
<Button
|
||||
icon="arrow-right"
|
||||
onClick={() =>
|
||||
handleColorUpdate(colorPair.input, 'output', index)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label">{`==`}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="arrow-right-arrow-left"
|
||||
onClick={() => {
|
||||
handleColorUpdate(colorPair.input, 'output', index);
|
||||
handleColorUpdate(colorPair.output, 'input', index);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label">{`=>`}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
onClick={() =>
|
||||
handleColorUpdate(colorPair.output, 'input', index)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box>Target</Box>
|
||||
@@ -164,7 +177,7 @@ export const ColorMateMatrixSolver = (props: {
|
||||
onClick={() =>
|
||||
onColorPairs([
|
||||
...colorPairs,
|
||||
{ input: '#ffffff', output: '#000000' },
|
||||
{ input: '#ffffff', output: '#ffffff' },
|
||||
])
|
||||
}
|
||||
>
|
||||
|
||||
@@ -5,56 +5,23 @@ export function isValidHex(hex: string): boolean {
|
||||
export function computeMatrixFromPairs(
|
||||
pairs: { input: string; output: string }[],
|
||||
): number[][] {
|
||||
const identityColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff'];
|
||||
const rgbIn = pairs.map((p) => hexToRgb255(p.input));
|
||||
const rgbOut = pairs.map((p) => hexToRgb255(p.output));
|
||||
|
||||
const workingPairs = [...pairs];
|
||||
|
||||
for (const idColor of identityColors) {
|
||||
const alreadyUsed = workingPairs.some(
|
||||
(p) => p.input.toLowerCase() === idColor,
|
||||
);
|
||||
if (!alreadyUsed) {
|
||||
workingPairs.push({
|
||||
input: idColor,
|
||||
output: idColor,
|
||||
});
|
||||
}
|
||||
if (rgbIn.length === 0) {
|
||||
throw new Error('At least one color pair is required');
|
||||
}
|
||||
|
||||
const rgbIn = workingPairs.map((p) => hexToRgb(p.input));
|
||||
const rgbOut = workingPairs.map((p) => hexToRgb(p.output));
|
||||
|
||||
const matrix: number[][] = [];
|
||||
|
||||
for (let channel = 0; channel < 3; channel++) {
|
||||
let attempts = 0;
|
||||
let weights: number[] = [];
|
||||
const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]);
|
||||
const b = rgbOut.map((rgb) => rgb[channel]);
|
||||
|
||||
while (attempts < 5) {
|
||||
try {
|
||||
const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]);
|
||||
const b = rgbOut.map((rgb) => rgb[channel]);
|
||||
|
||||
weights = leastSquares(a, b);
|
||||
|
||||
if (!weights.every((w) => w >= -10 && w <= 10)) {
|
||||
throw new Error('Computed weights out of range');
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (e) {
|
||||
const idx = rgbOut.length - 1;
|
||||
rgbOut[idx] = rgbOut[idx].map(
|
||||
(val) => val + Math.random() * 0.01 - 0.005,
|
||||
);
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
const weights = leastSquares(a, b);
|
||||
|
||||
if (weights.length !== 4) {
|
||||
throw new Error(
|
||||
`Matrix computation failed for channel ${channel} after ${attempts} attempts`,
|
||||
);
|
||||
throw new Error(`Matrix computation failed for channel ${channel}`);
|
||||
}
|
||||
|
||||
matrix.push(weights);
|
||||
@@ -63,66 +30,99 @@ export function computeMatrixFromPairs(
|
||||
return matrix;
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): number[] {
|
||||
const clean = hex.replace('#', '').padEnd(6, '0');
|
||||
const r = parseInt(clean.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(clean.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(clean.slice(4, 6), 16) / 255;
|
||||
function hexToRgb255(hex: string): number[] {
|
||||
const clean = hex.replace('#', '').padStart(6, '0');
|
||||
const r = parseInt(clean.slice(0, 2), 16);
|
||||
const g = parseInt(clean.slice(2, 4), 16);
|
||||
const b = parseInt(clean.slice(4, 6), 16);
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
function transpose(matrix: number[][]): number[][] {
|
||||
return matrix[0].map((_, i) => matrix.map((row) => row[i]));
|
||||
}
|
||||
function gaussElim(a: number[][], b: number[]): number[] {
|
||||
const n = a.length;
|
||||
const m = a[0].length;
|
||||
const aug = a.map((row, i) => [...row, b[i]]);
|
||||
|
||||
function multiply(a: number[][], b: number[][]): number[][] {
|
||||
const result: number[][] = Array(a.length)
|
||||
.fill(0)
|
||||
.map(() => Array(b[0].length).fill(0));
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
for (let j = 0; j < b[0].length; j++) {
|
||||
for (let k = 0; k < b.length; k++) {
|
||||
result[i][j] += a[i][k] * b[k][j];
|
||||
let rank = 0;
|
||||
for (let col = 0; col < m && rank < n; col++) {
|
||||
let best = rank;
|
||||
for (let row = rank + 1; row < n; row++) {
|
||||
if (Math.abs(aug[row][col]) > Math.abs(aug[best][col])) {
|
||||
best = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function inverse(matrix: number[][]): number[][] {
|
||||
const size = matrix.length;
|
||||
const augmented = matrix.map((row, i) => [
|
||||
...row,
|
||||
...Array(size)
|
||||
.fill(0)
|
||||
.map((_, j) => (i === j ? 1 : 0)),
|
||||
]);
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const diag = augmented[i][i];
|
||||
if (diag === 0) {
|
||||
throw new Error('Singular matrix');
|
||||
if (Math.abs(aug[best][col]) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < size * 2; j++) augmented[i][j] /= diag;
|
||||
for (let k = 0; k < size; k++) {
|
||||
if (k === i) continue;
|
||||
const factor = augmented[k][i];
|
||||
for (let j = 0; j < size * 2; j++) {
|
||||
augmented[k][j] -= factor * augmented[i][j];
|
||||
|
||||
[aug[rank], aug[best]] = [aug[best], aug[rank]];
|
||||
|
||||
for (let row = rank + 1; row < n; row++) {
|
||||
const factor = aug[row][col] / aug[rank][col];
|
||||
for (let j = col; j <= m; j++) {
|
||||
aug[row][j] -= factor * aug[rank][j];
|
||||
}
|
||||
}
|
||||
|
||||
rank++;
|
||||
}
|
||||
return augmented.map((row) => row.slice(size));
|
||||
|
||||
const x = new Array(m).fill(0);
|
||||
for (let i = Math.min(rank, m) - 1; i >= 0; i--) {
|
||||
if (Math.abs(aug[i][i]) < 1e-12) continue;
|
||||
let sum = aug[i][m];
|
||||
for (let j = i + 1; j < m; j++) {
|
||||
sum -= aug[i][j] * x[j];
|
||||
}
|
||||
x[i] = sum / aug[i][i];
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
function leastSquares(a: number[][], b: number[]): number[] {
|
||||
const AT = transpose(a);
|
||||
const ATa = multiply(AT, a);
|
||||
const ATb = multiply(
|
||||
AT,
|
||||
b.map((v) => [v]),
|
||||
const rows = a.length;
|
||||
const cols = a[0].length;
|
||||
|
||||
const ATa: number[][] = Array.from({ length: cols }, () =>
|
||||
new Array(cols).fill(0),
|
||||
);
|
||||
const ATainv = inverse(ATa);
|
||||
const result = multiply(ATainv, ATb);
|
||||
return result.map((r) => r[0]);
|
||||
const ATb = new Array(cols).fill(0);
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
for (let k = 0; k < rows; k++) {
|
||||
ATa[i][j] += a[k][i] * a[k][j];
|
||||
}
|
||||
}
|
||||
for (let k = 0; k < rows; k++) {
|
||||
ATb[i] += a[k][i] * b[k];
|
||||
}
|
||||
}
|
||||
|
||||
return gaussElim(ATa, ATb);
|
||||
}
|
||||
|
||||
export function transformColor(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
matrix: number[][],
|
||||
): number[] {
|
||||
const outR = clamp(
|
||||
matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b + matrix[0][3],
|
||||
);
|
||||
const outG = clamp(
|
||||
matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b + matrix[1][3],
|
||||
);
|
||||
const outB = clamp(
|
||||
matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b + matrix[2][3],
|
||||
);
|
||||
|
||||
return [outR, outG, outB];
|
||||
}
|
||||
|
||||
function clamp(v: number): number {
|
||||
return Math.max(0, Math.min(255, Math.round(v)));
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ColorMate = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const [colorPairs, setColorPairs] = useState([
|
||||
{ input: '#ffffff', output: '#000000' },
|
||||
{ input: '#ffffff', output: '#ffffff' },
|
||||
]);
|
||||
|
||||
const [activeID, setActiveId] = useState({ id: null, type: null });
|
||||
@@ -190,7 +190,7 @@ export const ColorMate = (props) => {
|
||||
<Stack.Item align="center">Coloring: {item_name}</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Stack fill mt={1}>
|
||||
<Stack.Item width="33%">
|
||||
<Stack.Item width="25%">
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
@@ -227,7 +227,7 @@ export const ColorMate = (props) => {
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item width="66%">
|
||||
<Stack.Item grow>
|
||||
{tab[activemode] || <Box textColor="red">Error</Box>}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user