From 955b0a890da51677ce77bb8873c2c58edddd911d Mon Sep 17 00:00:00 2001 From: Sebastian Seedorf Date: Thu, 4 Jun 2026 11:04:58 +0200 Subject: [PATCH] feat: item color map, fix regex matching, fix sort order, fix resize handle --- web/app/api/signals/route.ts | 53 +-- web/bin/fix-colors.ts | 87 +++++ web/components/ChartCard/SignalsChart.tsx | 7 +- web/components/ChartCard/TableViz.tsx | 11 +- web/components/ChartCard/plotHelpers.ts | 31 +- web/lib/colors.ts | 35 ++ web/public/factorio_item_colors.csv | 379 ++++++++++++++++++++++ 7 files changed, 562 insertions(+), 41 deletions(-) create mode 100644 web/bin/fix-colors.ts create mode 100644 web/lib/colors.ts create mode 100644 web/public/factorio_item_colors.csv diff --git a/web/app/api/signals/route.ts b/web/app/api/signals/route.ts index 7adf374..890a8ff 100644 --- a/web/app/api/signals/route.ts +++ b/web/app/api/signals/route.ts @@ -28,12 +28,15 @@ export const GET = withAuth(async (req: NextRequest) => { if (itemsWhitelist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); - // Each pattern is expanded to matching keys (tested against key AND localized name). - // Union all patterns — if a pattern matches nothing, it contributes no keys. - const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; - if (keys.length === 0) return NextResponse.json([]); - conditions.push(`item_key = ANY($${i++})`); - values.push(keys); + const localeKeys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; + const sqlPattern = itemsWhitelist.map(p => `(${p})`).join('|'); + const orConds = [`item_key ~* $${i++}`]; + values.push(sqlPattern); + if (localeKeys.length > 0) { + orConds.push(`item_key = ANY($${i++})`); + values.push(localeKeys); + } + conditions.push(`(${orConds.join(' OR ')})`); } else { conditions.push(`item_key = ANY($${i++})`); values.push(itemsWhitelist); @@ -43,12 +46,15 @@ export const GET = withAuth(async (req: NextRequest) => { if (itemsBlacklist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); - const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; - // If blacklist pattern matches nothing, nothing to exclude — skip condition - if (keys.length > 0) { - conditions.push(`item_key != ALL($${i++})`); - values.push(keys); + const localeKeys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; + const sqlPattern = itemsBlacklist.map(p => `(${p})`).join('|'); + const andConds = [`item_key !~* $${i++}`]; + values.push(sqlPattern); + if (localeKeys.length > 0) { + andConds.push(`item_key != ALL($${i++})`); + values.push(localeKeys); } + conditions.push(`(${andConds.join(' AND ')})`); } else { conditions.push(`item_key != ALL($${i++})`); values.push(itemsBlacklist); @@ -78,10 +84,15 @@ export const GET = withAuth(async (req: NextRequest) => { if (itemsWhitelist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); - const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; - if (keys.length === 0) return NextResponse.json([]); - baseConditions.push(`item_key = ANY($${j++})`); - baseValues.push(keys); + const localeKeys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; + const sqlPattern = itemsWhitelist.map(p => `(${p})`).join('|'); + const orConds = [`item_key ~* $${j++}`]; + baseValues.push(sqlPattern); + if (localeKeys.length > 0) { + orConds.push(`item_key = ANY($${j++})`); + baseValues.push(localeKeys); + } + baseConditions.push(`(${orConds.join(' OR ')})`); } else { baseConditions.push(`item_key = ANY($${j++})`); baseValues.push(itemsWhitelist); @@ -90,11 +101,15 @@ export const GET = withAuth(async (req: NextRequest) => { if (itemsBlacklist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); - const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; - if (keys.length > 0) { - baseConditions.push(`item_key != ALL($${j++})`); - baseValues.push(keys); + const localeKeys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; + const sqlPattern = itemsBlacklist.map(p => `(${p})`).join('|'); + const andConds = [`item_key !~* $${j++}`]; + baseValues.push(sqlPattern); + if (localeKeys.length > 0) { + andConds.push(`item_key != ALL($${j++})`); + baseValues.push(localeKeys); } + baseConditions.push(`(${andConds.join(' AND ')})`); } else { baseConditions.push(`item_key != ALL($${j++})`); baseValues.push(itemsBlacklist); diff --git a/web/bin/fix-colors.ts b/web/bin/fix-colors.ts new file mode 100644 index 0000000..d82a82d --- /dev/null +++ b/web/bin/fix-colors.ts @@ -0,0 +1,87 @@ +import { readFileSync, writeFileSync } from 'fs'; +const CSV = 'public/factorio_item_colors.csv'; + +function hexToHsl(h: string): [number, number, number] { + let r = parseInt(h.slice(1, 3), 16) / 255, g = parseInt(h.slice(3, 5), 16) / 255, b = parseInt(h.slice(5, 7), 16) / 255; + const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2; + if (mx === mn) return [0, 0, Math.round(l * 100)]; + const d = mx - mn, s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn); + let hue = 0; + if (mx === r) hue = ((g - b) / d + (g < b ? 6 : 0)) * 60; + else if (mx === g) hue = ((b - r) / d + 2) * 60; + else hue = ((r - g) / d + 4) * 60; + return [Math.round(hue), Math.round(s * 100), Math.round(l * 100)]; +} +function hslToHex(h: number, s: number, l: number): string { + s /= 100; l /= 100; + const c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = l - c / 2; + let r = 0, g = 0, b = 0; + if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; } + else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; } + const to = (v: number) => Math.round((v + m) * 255).toString(16).padStart(2, '0'); + return '#' + to(r) + to(g) + to(b); +} +function djb2(s: string): number { + let h = 5381; + for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i); + return h >>> 0; +} +function hueDist(a: number, b: number): number { + const d = Math.abs(a - b); + return Math.min(d, 360 - d); +} + +const lines = readFileSync(CSV, 'utf-8').trim().split('\n'); +const header = lines[0]; +const raw = lines.slice(1).filter(l => l.trim()).map(l => { + const [k, c] = l.split(','); + return { key: k.trim(), color: c.trim() }; +}); + +// Dedup by key +const seen = new Set(); +const rows: typeof raw = []; +for (const r of raw) { if (seen.has(r.key)) continue; seen.add(r.key); rows.push(r); } + +const n = rows.length; +const parent = Array.from({ length: n }, (_, i) => i); +function find(x: number): number { + while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } + return x; +} +function union(a: number, b: number) { parent[find(a)] = find(b); } + +const hsl = rows.map((r, i) => ({ ...r, hsl: hexToHsl(r.color), idx: i })); + +for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (hueDist(hsl[i].hsl[0], hsl[j].hsl[0]) <= 0.5 && + Math.abs(hsl[i].hsl[2] - hsl[j].hsl[2]) <= 0.5) + union(i, j); + } +} + +const groups = new Map(); +for (let i = 0; i < n; i++) { + const root = find(i); + if (!groups.has(root)) groups.set(root, []); + groups.get(root)!.push(hsl[i]); +} + +let fixed = 0; +for (const [, items] of groups) { + if (items.length < 2) continue; + items.sort((a, b) => a.idx - b.idx); + const [oh, os, ol] = items[0].hsl; + for (let i = 1; i < items.length; i++) { + const hash = djb2(items[i].key); + const h = (oh + (hash % 5 - 2) + i * 7 + 360) % 360; + const l = Math.max(0, Math.min(100, ol + ((hash >> 4) % 5 - 2))); + items[i].color = hslToHex(h, os, l); + fixed++; + } +} + +hsl.sort((a, b) => a.idx - b.idx); +writeFileSync(CSV, header + '\n' + hsl.map(r => `${r.key},${r.color}`).join('\n') + '\n'); +if (fixed) console.log(`Fixed ${fixed} close colors`); diff --git a/web/components/ChartCard/SignalsChart.tsx b/web/components/ChartCard/SignalsChart.tsx index c287261..b411274 100644 --- a/web/components/ChartCard/SignalsChart.tsx +++ b/web/components/ChartCard/SignalsChart.tsx @@ -2,8 +2,11 @@ import 'uplot/dist/uPlot.min.css'; import uPlot from 'uplot'; +import { useState, useEffect } from 'react'; import { useApp } from '@/lib/context'; import { resolveName } from '@/lib/localization'; +import { getColorMap } from '@/lib/colors'; +import type { ColorMap } from '@/lib/colors'; import { CardShell } from './CardShell'; import { makeYScale, makeAnnotationHooks, makeSignalsSeries, makeSignalsAxes, CURSOR_NO_DRAG } from './plotHelpers'; import { buildSeriesData } from './seriesData'; @@ -24,6 +27,8 @@ interface Props { export default function SignalsChart({ config, rows, sessions, alerts, timeMode, onEdit, onDelete }: Props) { const { localeMap } = useApp(); const locale = typeof navigator !== 'undefined' ? navigator.language : 'de-DE'; + const [colorMap, setColorMap] = useState(new Map()); + useEffect(() => { getColorMap().then(setColorMap); }, []); const { containerRef, legendRef } = usePlot( (el, w, h, lRef) => { @@ -45,7 +50,7 @@ export default function SignalsChart({ config, rows, sessions, alerts, timeMode, if (lRef.current) lRef.current.appendChild(legendEl); }, }, - series: makeSignalsSeries(keys, timeMode, key => resolveName(key, localeMap)), + series: makeSignalsSeries(keys, timeMode, key => resolveName(key, localeMap), colorMap), axes: makeSignalsAxes(timeMode, locale), scales: { x: { time: false }, diff --git a/web/components/ChartCard/TableViz.tsx b/web/components/ChartCard/TableViz.tsx index e41783a..735e437 100644 --- a/web/components/ChartCard/TableViz.tsx +++ b/web/components/ChartCard/TableViz.tsx @@ -1,6 +1,9 @@ +import { useState, useEffect } from 'react'; import { useApp } from '@/lib/context'; import { resolveName } from '@/lib/localization'; import { formatSI } from '@/lib/formatNumber'; +import { getColorMap, getItemColor } from '@/lib/colors'; +import type { ColorMap } from '@/lib/colors'; import { CardShell } from './CardShell'; import type { ChartConfig, SignalRow } from '@/lib/types'; @@ -13,6 +16,8 @@ interface Props { export default function TableViz({ config, rows, onEdit, onDelete }: Props) { const { localeMap } = useApp(); + const [colorMap, setColorMap] = useState(new Map()); + useEffect(() => { getColorMap().then(setColorMap); }, []); const latest = new Map(); for (const row of rows) { @@ -37,7 +42,11 @@ export default function TableViz({ config, rows, onEdit, onDelete }: Props) { const [combinator, item_key] = key.split('::'); return ( - {resolveName(item_key, localeMap)} + + + {resolveName(item_key, localeMap)} + {combinator} {config.signal_type !== 'red' && ( diff --git a/web/components/ChartCard/plotHelpers.ts b/web/components/ChartCard/plotHelpers.ts index 085839d..091a3f3 100644 --- a/web/components/ChartCard/plotHelpers.ts +++ b/web/components/ChartCard/plotHelpers.ts @@ -1,19 +1,8 @@ import uPlot from 'uplot'; import type { ChartConfig } from '@/lib/types'; import { formatSI } from '@/lib/formatNumber'; - -// --- Color helpers --- - -function djb2(s: string): number { - let h = 5381; - for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i); - return h >>> 0; -} - -function hslColor(key: string): string { - const hue = djb2(key) % 360; - return `hsl(${hue},70%,60%)`; -} +import type { ColorMap } from '@/lib/colors'; +import { getItemColor } from '@/lib/colors'; const SEMANTIC_GREEN = '#4ade80'; const SEMANTIC_RED = '#f87171'; @@ -24,10 +13,11 @@ export interface SeriesStyle { } export function getSeriesStyle( - key: string, - uCombs: number, - uItems: number, - uSigs: number, + key: string, + uCombs: number, + uItems: number, + uSigs: number, + colorMap: ColorMap = new Map(), ): SeriesStyle { const [combinator, item_key, sig] = key.split('::'); @@ -35,9 +25,9 @@ export function getSeriesStyle( return { color: sig === 'green' ? SEMANTIC_GREEN : SEMANTIC_RED, dash: undefined }; } if (uItems > 1) { - return { color: hslColor(item_key), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined }; + return { color: getItemColor(item_key, colorMap), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined }; } - return { color: hslColor(combinator), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined }; + return { color: getItemColor(combinator, colorMap), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined }; } /** @@ -137,6 +127,7 @@ export function makeSignalsSeries( keys: string[], timeMode: 'real' | 'tick', resolveName: (key: string) => string, + colorMap: ColorMap = new Map(), ): uPlot.Series[] { const uCombs = new Set(keys.map(k => k.split('::')[0])).size; const uItems = new Set(keys.map(k => k.split('::')[1])).size; @@ -154,7 +145,7 @@ export function makeSignalsSeries( xSeries, ...keys.map(k => { const [, item_key] = k.split('::'); - const { color, dash } = getSeriesStyle(k, uCombs, uItems, uSigs); + const { color, dash } = getSeriesStyle(k, uCombs, uItems, uSigs, colorMap); return { label: getSeriesLabel(k, uCombs, uItems, uSigs, resolveName(item_key)), stroke: color, diff --git a/web/lib/colors.ts b/web/lib/colors.ts new file mode 100644 index 0000000..5fde0c8 --- /dev/null +++ b/web/lib/colors.ts @@ -0,0 +1,35 @@ +export type ColorMap = Map; + +declare global { var __colorMapCache: ColorMap | undefined; } + +export function parseColorCsv(text: string): ColorMap { + const map: ColorMap = new Map(); + const lines = text.split(/\r?\n/).slice(1); + for (const line of lines) { + if (!line.trim()) continue; + const [key, color] = line.split(','); + if (key && color) map.set(key.trim(), color.trim()); + } + return map; +} + +export async function getColorMap(): Promise { + if (globalThis.__colorMapCache) return globalThis.__colorMapCache; + try { + const res = await fetch('/factorio_item_colors.csv'); + globalThis.__colorMapCache = res.ok ? parseColorCsv(await res.text()) : new Map(); + } catch { + globalThis.__colorMapCache = new Map(); + } + return globalThis.__colorMapCache; +} + +function djb2(s: string): number { + let h = 5381; + for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i); + return h >>> 0; +} + +export function getItemColor(key: string, colorMap: ColorMap): string { + return colorMap.get(key) ?? `hsl(${djb2(key) % 360},70%,60%)`; +} diff --git a/web/public/factorio_item_colors.csv b/web/public/factorio_item_colors.csv new file mode 100644 index 0000000..b1c24a1 --- /dev/null +++ b/web/public/factorio_item_colors.csv @@ -0,0 +1,379 @@ +item_key,color +copper-ore,#bf8040 +copper-plate,#b87333 +copper-cable,#e65100 +iron-ore,#8d6e63 +iron-plate,#bdbdbd +iron-gear-wheel,#a0a0a0 +iron-stick,#888888 +steel-plate,#757575 +steel-gear-wheel,#5a5a5a +steel-beam,#424242 +stone,#a1887f +stone-brick,#b8956a +coal,#37474f +uranium-ore,#66bb6a +uranium-238,#7cb342 +uranium-235,#64dd17 +crude-oil,#1a1a1a +heavy-oil,#4e342e +light-oil,#ff8a65 +petroleum-gas,#1a0030 +lubricant,#1b5e20 +sulfuric-acid,#b2dfdb +water,#1565c0 +steam,#e1f5fe +wood,#6d4c41 +raw-wood,#3e2723 +plastic-bar,#b0bec5 +sulfur,#fff176 +explosives,#ef5350 +battery,#ff9800 +empty-barrel,#90a4ae +filled-barrel,#546e7a +electronic-circuit,#4caf50 +advanced-circuit,#e53935 +processing-unit,#1565d2 +automation-science-pack,#d32f2f +logistic-science-pack,#43a047 +military-science-pack,#616161 +chemical-science-pack,#00acc1 +production-science-pack,#ff8f00 +utility-science-pack,#7b1fa2 +space-science-pack,#f48fb1 +pipe,#78909c +engine-unit,#795548 +electric-engine-unit,#4fc3f7 +flying-robot-frame,#5e35b1 +rocket-fuel,#ff5722 +rocket-control-unit,#2e7d32 +low-density-structure,#bcaaa4 +heat-pipe,#bf360c +heat-exchanger,#263238 +steam-turbine,#455a64 +concrete,#9e9e9e +refined-concrete,#6d6d6d +landfill,#388e3c +cliff-explosives,#d84315 +nuclear-fuel,#00c853 +solid-fuel,#0d47a1 +grenade,#5d4037 +cluster-grenade,#c62828 +landmine,#4a148c +fish,#29b6f6 +glass,#4dd0e1 +rail,#9e9e9e +rail-signal,#4caf50 +rail-chain-signal,#e53935 +train-stop,#37474f +locomotive,#d32f2f +cargo-wagon,#8d6e63 +fluid-wagon,#1565c0 +artillery-wagon,#424242 +artillery-turret,#424242 +flamethrower-turret,#d84315 +gun-turret,#616161 +laser-turret,#e53935 +radar,#388e3c +roboport,#2196f3 +construction-robot,#4caf50 +logistic-robot,#1565c0 +speed-module,#f44336 +speed-module-2,#e91e63 +speed-module-3,#9c27b0 +speed-module-5,#4a148c +effectivity-module,#4caf50 +effectivity-module-2,#388e3c +effectivity-module-3,#2e7d32 +effectivity-module-4,#1b5e20 +productivity-module,#ff9800 +productivity-module-2,#ef6c00 +productivity-module-3,#e65100 +productivity-module-5,#bf360c +beacon,#2196f3 +substation,#9c27b0 +medium-electric-pole,#78909c +big-electric-pole,#546e7a +small-electric-pole,#90a4ae +small-iron-electric-pole,#90a4ae +steel-chest,#757575 +iron-chest,#8d6e63 +wooden-chest,#6d4c41 +transport-belt,#bdbdbd +fast-transport-belt,#4fc3f7 +express-transport-belt,#e53935 +underground-belt,#bdbdbd +fast-underground-belt,#4fc3f7 +express-underground-belt,#e53935 +splitter,#bdbdbd +fast-splitter,#4fc3f7 +express-splitter,#e53935 +long-handed-inserter,#795548 +fast-inserter,#795548 +burner-inserter,#795548 +inserter,#795548 +stack-inserter,#4e342e +burner-mining-drill,#795548 +electric-mining-drill,#795548 +area-mining-drill,#795548 +electric-furnace,#455a64 +industrial-furnace,#455a64 +assembling-machine-3,#616161 +centrifuge,#546e7a +chemical-plant,#00acc1 +oil-refinery,#1a1a1a +pumpjack,#4e342e +offshore-pump,#1565c0 +boiler,#9e9e9e +steam-engine,#78909c +solar-panel,#ffb74d +accumulator,#2196f3 +lamp,#ffeb3b +constant-combinator,#546e7a +decider-combinator,#546e7a +arithmetic-combinator,#546e7a +power-switch,#ff8f00 +programmable-speaker,#7b1fa2 +aai-signal-receiver,#37474f +aai-signal-transmitter,#37474f +textplate-small-copper,#e65100 +used-up-uranium-fuel-cell,#37474f +uranium-fuel-cell,#66bb6a +electric-motor,#4fc3f7 +motor,#4fc3f7 +automation-core,#1565c0 +iron-beam,#888888 +kr-advanced-solar-panel,#ffb74d +kr-advanced-transport-belt,#26c6da +kr-advanced-loader,#00acc1 +kr-advanced-splitter,#00bcd4 +kr-superior-inserter,#795548 +kr-superior-filter-inserter,#5d4037 +kr-superior-long-inserter,#795548 +kr-superior-long-filter-inserter,#5d4037 +kr-superior-underground-belt,#00acc1 +kr-superior-loader,#00acc1 +kr-fuel-refinery,#bf360c +kr-quarry-drill,#795548 +kr-express-loader,#00838f +kr-electric-mining-drill-mk2,#4e342e +kr-steel-pipe,#757575 +kr-steel-pipe-to-ground,#757575 +kr-fluid-storage-2,#1565c0 +kr-se-loader,#00acc1 +beryllium,#81c784 +beryllium-ore,#558b2f +beryllium-sulfate,#00695c +holmium,#ba68c8 +holmium-ore,#ab47bc +holmium-solution,#e1bee7 +cryonite,#00bcd4 +cryonite-rod,#26c6da +cryonite-solution,#80deea +iridium,#cfd8dc +iridium-ore,#689f38 +iridium-ingot,#e0e0e0 +vulcanite,#ff7043 +vulcanite-block,#e64a19 +vulcanite-powder,#ffab91 +imersite,#8e24aa +imersite-crystal,#283593 +imersite-powder,#ce93d8 +vita,#8bc34a +vita-extract,#1b5e20 +vita-germination,#006064 +naquium,#ffd54f +naquium-ore,#ffca28 +naquium-ingot,#fff9c4 +methane-ice,#b2ebf2 +core-fragment,#4db6ac +rare-metals,#b0b0b0 +raw-rare-metals,#b0b0b0 +raw-imersite,#8e24aa +sand,#c2b280 +silicon,#78909c +quartz,#e0e0e0 +coke,#424242 +se-copper-ingot,#b87333 +se-iron-ingot,#8d6e63 +se-steel-ingot,#757575 +se-beryllium-ingot,#81c784 +se-holmium-ingot,#ba68c8 +se-iridium-ingot,#cfd8dc +se-cryonite-rod,#26c6da +se-cryonite-slush,#80deea +se-vulcanite-crushed,#ff7043 +se-vulcanite-enriched,#ff5722 +se-vulcanite-block,#e64a19 +imersium-plate,#7b1fa2 +se-kr-imersium-sulfide,#7b1fa2 +se-kr-fine-imersite-powder,#ce93d8 +enriched-iron,#8d6e63 +electronic-components,#4caf50 +empty-data-card,#512da8 +heat-shielding,#00bfa5 +thermodynamic-boiler,#311b92 +cryogenic-plant,#0097a7 +core-drill,#3d5afe +core-miner,#1a237e +se-quantum-processor,#1565d2 +se-holmium-cable,#ba68c8 +se-holmium-solenoid,#ba68c8 +se-superconductive-cable,#1a237e +se-data-storage-substrate,#4527a0 +se-machine-learning-data,#3949ab +se-empty-data,#5c6bc0 +se-broken-data,#7986cb +se-junk-data,#9fa8da +se-scrap,#757575 +se-contaminated-scrap,#4e342e +se-genetic-data,#66bb6a +se-significant-data,#43a047 +se-experimental-genetic-data,#2e7d32 +se-atomic-data,#00bcd4 +se-star-probe-data,#ffd54f +se-significant-specimen,#00bfa5 +se-specimen,#26a69a +se-bio-sludge,#2e7d32 +se-nutrient-gel,#00695c +se-nutrient-gel-barrel,#004d40 +mineral-water,#4fc3f7 +chlorine,#b2dfdb +nitric-acid,#ffcc80 +se-bioscrubber,#1b5e20 +se-space-coolant-hot,#d84315 +se-space-water,#4fc3f7 +se-chemical-gel,#8e24aa +se-vitalic-acid,#8bc34a +se-vitalic-reagent,#7cb342 +se-vitalic-epoxy,#689f38 +se-neural-gel,#b39ddb +se-neural-gel-2,#9575cd +se-plasma-stream,#d50000 +se-proton-stream,#e040fb +se-ion-stream,#651fff +se-particle-stream,#304ffe +se-vitamelange-extract,#558b2f +se-vitamelange-spice,#689f38 +se-water-ice,#e3f2fd +lithium,#b0bec5 +lithium-chloride,#b0bec5 +lithium-sulfur-battery,#ce93d8 +fertilizer,#4e342e +biomass,#33691e +biomethanol,#43a047 +se-surface-teleporter,#1a237e +se-observation-frame,#7cb342 +se-observation-frame-blank,#8bc34a +se-core-fragment-se-beryllium,#81c784 +se-core-fragment-se-cryonite,#00bcd4 +se-core-fragment-se-holmium,#ba68c8 +se-core-fragment-se-imersite,#8e24aa +se-core-fragment-se-iridium,#cfd8dc +se-core-fragment-se-naquium,#ffca28 +se-core-fragment-se-vita,#8bc34a +se-core-fragment-se-vulcanite,#e64a19 +se-core-fragment-omni,#4db6ac +se-core-fragment-se-iridium-ore,#689f38 +se-core-fragment-se-vitamelange,#8bc34a +se-aeroframe-bulkhead,#4db6ac +se-aeroframe-scaffold,#26a69a +se-aeroframe-pole,#00897b +se-heavy-girder,#78909c +se-heavy-bearing,#607d8b +se-heavy-composite,#546e7a +se-space-pipe,#4db6ac +se-space-transport-belt,#26c6da +se-space-underground-belt,#00acc1 +se-space-splitter,#00bcd4 +se-space-accumulator,#4fc3f7 +se-space-solar-panel,#ffb74d +se-space-solar-panel-2,#ffa726 +se-space-elevator-cable,#4527a0 +se-space-pipe-to-ground,#4db6ac +se-space-mirror,#e0e0e0 +se-space-rail,#78909c +se-space-platform-scaffold,#bdbdbd +se-space-probe-rocket,#ff7043 +se-space-capsule,#42a5f5 +se-meteor-defence-ammo,#ef5350 +se-dynamic-emitter,#1565c0 +se-gammaray-detector,#00bfa5 +se-pylon-substation,#ffd54f +se-rocket-launch-pad,#757575 +se-rocket-landing-pad,#616161 +se-cargo-rocket-fuel-tank,#ff5722 +se-cargo-rocket-cargo-pod,#ff8a65 +se-cargo-rocket-section,#ffab91 +se-cargo-rocket-section-packed,#ffcc80 +se-lifesupport-canister,#4fc3f7 +se-used-lifesupport-canister,#90a4ae +se-canister,#90a4ae +se-magnetic-canister,#ba68c8 +se-iridium-ore,#689f38 +se-iridium-ore-crushed,#689f38 +se-beryllium-ore,#558b2f +se-beryllium-sulfate,#00695c +se-holmium-ore,#ab47bc +se-holmium-ore-crushed,#ab47bc +se-compact-beacon,#1565c0 +se-recycling-facility,#4caf50 +se-rocket-science-pack,#e0e0e0 +lubricant-barrel,#ffc107 +heavy-oil-barrel,#4e342e +light-oil-barrel,#ff8a65 +petroleum-gas-barrel,#1a0030 +se-material-science-pack-1,#ffb74d +se-material-science-pack-2,#ffa726 +se-material-science-pack-3,#ff9800 +se-material-science-pack-4,#ef6c00 +se-material-testing-pack,#f57c00 +se-material-insight,#ffe0b2 +se-material-catalogue-1,#ffb74d +se-material-catalogue-2,#ffa726 +se-material-catalogue-3,#ff9800 +se-material-catalogue-4,#ef6c00 +se-astronomic-science-pack-1,#5c6bc0 +se-astronomic-science-pack-2,#3949ab +se-astronomic-insight,#c5cae9 +se-astronomic-catalogue-1,#5c6bc0 +se-astronomic-catalogue-2,#3949ab +se-astronomic-catalogue-3,#283593 +se-astronomic-catalogue-4,#1a237e +se-biological-science-pack-1,#69f0ae +se-biological-science-pack-2,#00e676 +se-biological-science-pack-3,#00c853 +se-biological-science-pack-4,#009624 +se-biological-insight,#b9f6ca +se-biological-catalogue-1,#69f0ae +se-biological-catalogue-2,#00e676 +se-biological-catalogue-3,#00c853 +se-biological-catalogue-4,#009624 +se-energy-science-pack-1,#f48fb1 +se-energy-science-pack-2,#f06292 +se-energy-science-pack-3,#ec407a +se-energy-science-pack-4,#d81b60 +se-energy-insight,#fce4ec +se-energy-catalogue-1,#f48fb1 +se-energy-catalogue-2,#f06292 +se-energy-catalogue-3,#ec407a +se-energy-catalogue-4,#d81b60 +se-deep-space-science-pack-1,#b39ddb +se-deep-space-science-pack-2,#9575cd +se-deep-space-science-pack-3,#7e57c2 +se-deep-space-science-pack-4,#5e35b1 +se-kr-matter-science-pack-1,#ffcc80 +se-kr-matter-science-pack-2,#ffb74d +se-kr-matter-liberation-data,#ffe0b2 +blank-tech-card,#90a4ae +singularity-tech-card,#1a237e +ltn-combinator,#1a237e +ltn-stop,#37474f +ltn-delivery-address,#2e7d32 +ltn-provider-stack-threshold,#f44336 +ltn-requester-stack-threshold,#2196f3 +ltn-provider-threshold,#d32f2f +ltn-requester-threshold,#1565c0 +ltn-max-trains,#4a148c +ltn-max-train-length,#4a148c +ltn-locked-slots,#546e7a