This page is used for previewing and submitting scripts for use with the Script Manager
Available Scripts
(2) TPTMulti by LBPHacker
(3) set wifi v2 by jacob1
(4) Script Paste by jacob1
(5) Random Element by jacob1
(6) Magical Merge Master 3000 by nucular
(7) More Fuel Mod-heavy by jward212
(9) Breakpoints (BRPT) by boxmein
(10) Cockroaches! by boxmein
(11) Random tree by ssccsscc
(12) Element with random properties by ssccsscc
(15) Minimalistic Element Dehider by nucular
(16) FPS Gauge by mniip
(17) TPT Radio by jward212
(18) Print Debugger by FeynmanLogomaker
(19) Powered BHOL by jacob1
(20) Light and lamps by electronic_steve
(21) Extremely Durable TTAN by QuentinADay
(22) New Buttons by QuentinADay
(23) Pure Energy by QuentinADay
(25) RCA's HUD XV Update I by RCAProduction
(27) 123456787654 vols by mjpowder
(28) Singularity Bomb by QuentinADay
(29) ES wifi set by electronic_steve
(30) Lua Elements Pack by FeynmanLogomaker
(31) stkm gun by jward212
(32) space building materials by kjack1111
(34) Rust bomb by Damian97
(35) Simple command block by ssccsscc
(36) Everlasting Fusion by QuentinADay
(37) Screenshot Organiser by mecha-man
(38) Napalm mod by cccp3
(39) Rocket fuel mod v0.15 by cccp3
(41) Useful web links by jward212
(42) TPT Logic Gates Mod by iamdumb
(43) ESTools by electronic_steve
(44) Head Crabs-HL2 by jward212
(45) Procedural Save Generator by boxmein
(46) smooth colours for nametag by jward212
(47) ZAKPACK by zak03
(49) Performance Monitor by FeynmanLogomaker
(51) Texter by byzod
(52) Texter default fonts by byzod
(53) Schicko's Font Pack for Texter by Schicko
(54) Realistic Element Names by Atomic10
(55) TPT's Mod V.3 Update 1 by Amy
(56) Temporaryaccount-Decorator by Temporaryaccount
(57) random save loader by jward212
(58) Tmp gradient display by ssccsscc
(59) particle re-orderer by mniip
(60) Electric Glow by jacob1
(61) More Fuel Mod-lite by jward212
(63) Rythidium by janekbe04
(64) Simple FPS GUI by Sfsjunior
(65) Enhanced Element Dehider by ChargedCreeper
(66) Graph of average temp by ssccsscc
(67) Template save loader by jacob1
(68) Lua Text Generator by jBot-42
(70) Pixel's Freezer by Pixelguru26
(71) Thingy | Fusion For Ever by TheChosenEvilOne
(72) op explosions by zolly_bro
(73) Scar by DorkyBobster
(74) ScreenShotMod by lill74
(77) Useful Things by TheEvilChosenOne
(78) Alchemy Mod by _MrN_
(79) Nuke v2 by Fnaf65
(80) Compressor mod by TheChosenEvilOne
(81) Custom Render Mode Loader by jacob1
(82) Spacewars by JosephMA
(83) MOAR - Alpha 0.1 by TheChosenEvilOne
(86) Element Creator by cxi
(88) Soapworm by LBPHacker
(90) Pressure Bomb by God_Kra
(91) SMFB by wntjq69
(92) Potato by cxi
(93) Subatomic Pack (BDS) by TPT_PL
(94) Acidic Pack (BDS) by TPT_PL
(95) Starbound Building Materials by Sanpypr
(96) Factory problems by TPT_PL
(97) Gamma Ray-diation by Kostia4381
(98) Magic by livingfossil
(99) Cross-window Copy/Cut/Paste by LBPHacker
(100) Langton's Ant with variations by LBPHacker
(101) Remote particle creator/deleter by TPT_PL
(102) Force fields by electronic_steve
(103) Reinforced Concrete by 12Me21
(105) TPT_PL's Lua Mod by TPT_PL
(106) Nuke v4 by Fnaf65
(107) CHEMMOD V1 by KevAK
(108) Chemicals by Ligan
(109) VonDaniel's Template by VonDaniel
(110) The Inaccurate Radioactivity Toy Mod by TuDoR2007
(112) textmonsterPack by textmonster404
(113) Meteor by TheScienceKid
(114) Tgpm by TuDoR2007
(115) Civilizations by TPT_PL
(116) RAD-MOD 1.2.1B by Kev_AK
(117) MicroLua by RamiLego4Game
(118) Extra customizable HUD by djy0212
(119) Ingame brush editor by ssccsscc
(120) Window Maker by Paul_31415
(121) CHEM-MOD V1.2B by Kev_AK
(122) Rainbow PHOT by Mrprocom
(123) stronger stickmanv by yuval
(124) 3D Pressure Visualizer by mniip
(125) Arkadian Liquid by JanKaszanka
(126) Fuel by nukers473
(127) Immersive Radioactivity v2.1 by Potbelly
(128) ElementLaunchingTool by juh9870
(129) CHEM-MOD_v1.2.2b by KevAK
(130) Slingshot by Mrprocom
(131) Perlin Noise Generator by DoubleF
(132) Element Replace by 159819
(133) Flooder V2 by TheAwesomeMutant
(134) Link Sign GUI by QuanTech
(135) Element dehider by 4e616d65
(136) Subphoton ROM Builder by mad-cow
(137) Hardened Dust by Liftski
(138) Bio-Vir by TheAwesomeMutant
(140) Orbit Simulator by Mrprocom
(141) johnnyou's Font for Texter by johnnyou (49796346)
(142) auto_wifi by phisically
(143) Layering helper by ssccsscc
(144) Layering Helper Extended by LuaMaster
(145) TPT Remade by TuDoR2007
(146) All-seeing sampler by djy0212
(147) Layering helper remastered by ssccsscc
(148) Eraser by thepowdertoy12
(149) EXPLOSIONS by olix3001
(150) Simple rocket fuel mod by ArseniyPlotnikov2006
(151) Pure Fission by Fnaf65
(154) Graph by ssccsscc
(155) Little's Pack! by LittleProgramming
(156) Lead by LoftisGaming
(157) WIFI Tuner by ssccsscc
(158) Previous Brush by 159819
(159) HUD Auto-Hider by Tim
(161) Stack tool by thepowdertoy12
(162) Oil and plastic by ArseniyPlotnikov2006
(163) Colored Ember by DUC
(164) Timer by ssccsscc
(165) Bacteria Mod by TuDoR2007
(166) Noise filter by LBPHacker
(167) Future-proof element dehider by LBPHacker
(168) RadioactiveNuke by DreamingWarlord
(169) Only Hot Element by DreamingWarlord
(170) Philosopher's Stone by Godhydra
(171) Conic section generator by LBPHacker
(172) Interface API by ssccsscc
(173) Metals&Materials by Ferrous26
(174) tpt.all by LBPHacker
(175) The Visual Elements Pack by Goblin01, vvv331
(176) Layering Helper Reforged by PowderNotSolid
(177) SNOWified SING by LBPHacker
(178) FPS Chart by Goblin01
(179) TPT font writer by Goblin01
(180) Simple Ruler by PowderNotSolid
(181) Heat Modifier by DreamingWarlord
(182) TPT Remade II by TuDoR2007
(183) Gravity simulator by ArseniyPlotnikov2k6
(184) Unobtainium by christheboss894
(185) TPTMIDI noteblock in tpt by djy0212
(186) DreamingWarlord's Lua Tool by DreamingWarlord
(187) Elements Tooltip by Goblin01
(189) Explodium script by 0d15ea5ebe57c0debadc0ffee0ddf00d
(190) more powered force elements by 6nop6nop
(191) Yzaak1Scifire Modpack by Yzaak1Scifire
(194) Fluor and more modpack! by galaktor
(195) Hot Powder by lieve_blendi
(196) Star by TUANMINHVIETNAM
(197) Heat Powders by lieve_blendi
(200) Tangeriinium (thx 2 cxi 4 code) by LostEditor
(201) Freezer by lieve_blendi
(202) Powder Power! by TPTSortaGuy
(203) PowderPlus v1.4 by PowderPlus Team
(204) fire by ME
(205) Stacked Goo Animations by Maticzpl
(206) Stickman Control for Android Version by PhauloRiquelme
(207) Spark Removal Button by Xyz
(208) More HEAC's! by Maxhd1234
(209) Immersive Radioactivity v3.0 by Potbelly
(210) Subframe Chipmaker Script by Maticzpl
(211) Realistic Propellants by ArseniyPlotnikov2k6
(212) Mass Equals Gravity by Maticzpl
(213) PhiMod v1 by ArolaunTech
(214) PC Controls for Android by Cracker1000
(217) Single-pixel pipe configurator by LBPHacker
(218) Omega Death Laser Gun by Dogeoum
(219) Notifications by Maticzpl
(221) Powderizer by ArolaunTech
(222) ElemDehider 1.2 by Inventor70
(223) Unobtainum V2 by DoomKittyAttack
(224) Organics Mod v0.2B by PowderPlus Team
(225) Gravity distortion by Avolte55
(226) tmp Wifi by PhauloRiquelme
(227) Alchemagica Mod v1.0 by RebMiami
(228) Fan Elements Mod by RebMiami
(229) Impossibilities by ArolaunTech
(230) Realistic Explosives by ArseniyPlotnikov2k6
(231) libactivation by anamorphic
(232) Alloy Brushes by Maticzpl
(233) Gravity bender by pres
(234) Slow Tick by Pixel
(235) Paste ID by Maticzpl
(236) many things by jadenflp2
(237) Territect by Rebmiami
(238) Better Descriptions v1.0.5 by ashyboi2022
(239) LIGHTNING SPRK by GOLmaster10101
(240) Small Bombs by juh9870
(241) Save Shop by aaccbb
(242) Moving solids v1.3.0 Beta by ArolaunTech
(244) Alchemistry by rdococ
(245) ETRD-IM by aaccbb
(246) RadonX by Justadirtblock
(248) Water-X by deuterium_oxide
(250) Indestructible INSL by CheekyRand0m
(252) Console's Mod by Console/Compec
(255) Slow motion by LBPHacker
(256) Powered Repeller by Hythonia
(257) Zeta's Electric Tools. by Zetalasis
(258) Azure serum (AZSR) by ALumpOfPowderToy
(259) COLORFULSAND by xert
(260) Lightning Circle by defaultuser0
(261) Powder Future Tech by JonaHungary
(262) TPTGlowingSolids by DestinyDyson
(263) Volcano Bomb by I_am_the_NugsWorld
(264) Neon Lights by Rebmiami
(265) Radioactive Materials by xyz
(266) Eater mod by VIPERGAMEZ
(267) the biology mod by someone
(268) Atomic Physics by qe
(269) Pure Radiation by ronansb
(270) Fake Elements by That_PowderToy_Guy
(271) Tachyons and MISC by RamenNoods
(272) Exotic Particles by rdococ
(273) FPS Slider by aaccbb
(274) Enphosian's Radioactive mod pack by Enphosian
(275) TPTMulti v2.2.5 Beta by LBPHacker
(276) ROM Builder by QnpfvTPz
(277) acb's Idea Generator by aaccbb
(278) Bode's Grapher by Bodester
(279) Gradient Tool by Bodester
(280) Better FPS Slider by ngmoco
(281) MyWIFI by Bodester
(282) acb's Slow-Mo Script by aaccbb
(283) Fun n' Chemicals by 0xHenryMC
(284) Resistant coating by git4rker
(285) Nationwide script by Kit237
(286) weird alphabet.lua by alice_loona_ot12
(287) MT's font for texter by Kit237
(288) Compiler for the SCE computer by NoVIcE by NoVIcE
(289) Perlin Noise Map Generator by Kit236
(290) hell fire element by Hecker
(291) Power stuff by William
(292) Missile by BaCOn
(293) Wargame's things by Kit236
(294) Inert Gas by EwguhMicBewguhGuy
(295) Average Temperature Display by tptQuantification
(296) Radicons by stillthere
(297) ROCKET by stillthere
(298) NUUKE! by stillthere
(299) SubpixelScope by Rebmiami
(300) Unobtanium Element Pack by AlgoPowdo
(301) Magnets by TheSuperNova
(302) library007 3000 by 008cff
(303) Element tuner by TheSuperNova
(304) Somethings by TheSuperNova
(305) Realistic Water by yuht
(306) Futurism by TheSuperNova
(307) MMod by MeltedLawnFlamingo
(308) Wierd energy by mellowmonster12
(309) Time Reversing by crocodilea
(310) Weather System by Bigmann
(311) infinite framerate by kayra
(312) Diamondifier mod by Believer0914
(313) Radtech by ReallyJustDont
(314) TPT 'port' of IRC Crackbot game by creator_of_fail
(315) Fireshoas Stuff by Fireshoa
(316) TPTASM: Universal assembler for TPT computers by LBPHacker
(317) Ronansb's Elements by ronansb
(318) AeroStuff v1.2 by AerospaceFan
(319) TPT Font Writer Expanded by budc123
(320) Element Table Pack by RetroBoy2040
(321) One Time Conductor by Believer0914
(322) Extra Debug Info by creator_of_fail
(323) burst v1 by ml_lom
(324) Nuclide Expansion by Alchnoious
(325) Nuclear Elems by xhrqnx2
(326) UM by Sunshine1304
(327) Android stamp rescanner by LBPHacker
(328) Temp Resetting Button by DoobsterBro1234
(329) INFEKTOR_infection_V1 by PowderTechnician1475
(330) Nanotech V2 by TheSuperNova
(331) super explosives by egglegendbro
(332) The Return of the Powder Dinn by savask
(333) INFEKTOR_infection_V2 by PowderTechnician1475
(334) Metallic Diamond by Shyth
(335) stupid stuff mod by stillthere
(336) R3 Keyboard Writer by lesio
(337) Chaos Matter by PstnPopper-2000
(338) VirusStuff by AerospaceFan
(339) acids and reactions by egglegendbro
(340) PipeHUD by JonaHungary
(341) visible warp by TheFPSKiller
(342) Ozone Mod by Aeiouman
(343) Leaf's Beautiful World by Leaf1
(344) Wireless FILT serializator by Houseinik
(345) TINY-PLATFORMER by LogikDOS
(346) RadiThor by linfuciuscont
(347) Aero's Periodic Table Mod by AerospaceFan
(348) MoreCommands by LogikDOS
(349) Line Tool+ by Saturnity
(350) MetalsPack by somedudesaidTPT
+ Submit new script!
Title:
Author:
Script:
do
local haveApis = (event ~= nil) and (evt ~= nil) and (graphics ~= nil)
and (sim ~= nil) and (tpt ~= nil) and (sim.createLine ~= nil)
and (ui ~= nil) and (ui.SDLK_c ~= nil)
if not haveApis then
local msg = "Line Tool+: required API missing (event/evt/graphics/sim/tpt/ui/sim.createLine), mod disabled"
if tpt and tpt.log then tpt.log(msg) end
print(msg)
return
end
local abs, floor, ceil, min, max, sqrt = math.abs, math.floor, math.ceil, math.min, math.max, math.sqrt
local atan2, sin, cos, rad, deg = math.atan2, math.sin, math.cos, math.rad, math.deg
local GRAB_R2 = 16
local COL_CYAN = {0, 255, 255, 255}
local COL_ORANGE = {255, 140, 0, 255}
local COL_YELLOW = {255, 255, 0, 255}
local COL_DIMYELLOW = {180, 180, 60, 255}
local COL_WHITE_PREVIEW = {255, 255, 255, 180}
local COL_DIMGRAY = {150, 150, 150, 255}
local COL_GREEN = {120, 220, 120, 255}
local CATEGORIES = {"Line", "Shape"}
local CATEGORY_MODES = {
Line = {"QUAD", "CUBIC", "ARC", "CUSTOM"},
Shape = {"CUSTOM", "TRI", "SQUARE", "PENT", "HEX", "OCT", "STAR", "CIRCLE"},
}
local DEFAULT_ARC_ANGLE = 90
local SHAPE_DEFS = {
TRI = {n = 3},
SQUARE = {n = 4, angleOffset = 45},
PENT = {n = 5},
HEX = {n = 6, angleOffset = 30},
OCT = {n = 8, angleOffset = 22.5},
STAR = {n = 5, inner = 0.4},
CIRCLE = {n = 48},
}
local DOCK_POSITIONS = {"top-left", "top-center", "top-right", "bottom-left", "bottom-right"}
local SNAP_VALUES = {5, 10, 15, 30, 45, 90}
local GRID_SIZE = 8
local DEFAULT_DOCK = "bottom-left"
local DEFAULT_SNAP_IDX = 3
local DEFAULT_CATEGORY = "Line"
local DEFAULT_MODE = "QUAD"
local DEFAULT_GRID = false
local DEFAULT_FILL = false
local DEFAULT_REPEAT = false
local DEFAULT_MIRROR = false
local DEFAULT_MIRROR_AXIS = "V"
local MOBILE_AUTO = (plat and plat.platform and plat.platform() == "ANDROID") or false
local K_ESCAPE = ui.SDLK_ESCAPE
local BIND_ACTIONS = {
{name = "toggle", label = "Toggle tool on/off", default = ui.SDLK_c},
{name = "settings", label = "Open/close settings", default = ui.SDLK_s},
{name = "cycle_category", label = "Cycle category", default = ui.SDLK_m},
{name = "toggle_closed", label = "Toggle closed shape", default = ui.SDLK_l},
{name = "toggle_grid", label = "Toggle grid snap", default = ui.SDLK_g},
{name = "toggle_fill", label = "Toggle fill", default = ui.SDLK_v},
{name = "toggle_repeat", label = "Toggle repeat", default = ui.SDLK_r},
{name = "cycle_mode", label = "Cycle mode", default = ui.SDLK_TAB},
{name = "select_mode_1", label = "Select mode 1", default = ui.SDLK_1},
{name = "select_mode_2", label = "Select mode 2", default = ui.SDLK_2},
{name = "select_mode_3", label = "Select mode 3", default = ui.SDLK_3},
{name = "select_mode_4", label = "Select mode 4", default = ui.SDLK_4},
{name = "select_mode_5", label = "Select mode 5", default = ui.SDLK_5},
{name = "select_mode_6", label = "Select mode 6", default = ui.SDLK_6},
{name = "select_mode_7", label = "Select mode 7", default = ui.SDLK_7},
{name = "select_mode_8", label = "Select mode 8", default = ui.SDLK_8},
{name = "remove_point", label = "Remove last point", default = ui.SDLK_BACKSPACE},
{name = "commit", label = "Commit shape", default = ui.SDLK_RETURN},
{name = "arc_angle_inc", label = "Arc angle +5", default = ui.SDLK_EQUALS},
{name = "arc_angle_dec", label = "Arc angle -5", default = ui.SDLK_MINUS},
{name = "fine_inc", label = "Fine adjust +1", default = ui.SDLK_PERIOD},
{name = "fine_dec", label = "Fine adjust -1", default = ui.SDLK_COMMA},
{name = "rotate_cw", label = "Rotate preset +5", default = ui.SDLK_RIGHTBRACKET},
{name = "rotate_ccw", label = "Rotate preset -5", default = ui.SDLK_LEFTBRACKET},
{name = "flip_cycle", label = "Flip (cycle V/H/HV/off)", default = ui.SDLK_f},
{name = "flip_h", label = "Flip horizontal (in-progress)", default = nil},
{name = "flip_v", label = "Flip vertical (in-progress)", default = nil},
{name = "flip_hv", label = "Flip both axes (in-progress)", default = nil},
{name = "toggle_mirror", label = "Toggle mirror", default = ui.SDLK_SEMICOLON},
{name = "cycle_mirror_axis", label = "Cycle mirror axis", default = ui.SDLK_QUOTE},
}
local BIND_LOOKUP = {}
for _, a in ipairs(BIND_ACTIONS) do BIND_LOOKUP[a.name] = a end
local ROWS_PER_PAGE = 8
local SPECIAL_KEY_NAMES = {
[ui.SDLK_TAB] = "Tab",
[ui.SDLK_RETURN] = "Return",
[ui.SDLK_ESCAPE] = "Esc",
[ui.SDLK_BACKSPACE] = "Backspace",
[ui.SDLK_SPACE] = "Space",
[ui.SDLK_CAPSLOCK] = "CapsLock",
[ui.SDLK_F1] = "F1", [ui.SDLK_F2] = "F2", [ui.SDLK_F3] = "F3", [ui.SDLK_F4] = "F4",
[ui.SDLK_F5] = "F5", [ui.SDLK_F6] = "F6", [ui.SDLK_F7] = "F7", [ui.SDLK_F8] = "F8",
[ui.SDLK_F9] = "F9", [ui.SDLK_F10] = "F10", [ui.SDLK_F11] = "F11", [ui.SDLK_F12] = "F12",
[ui.SDLK_INSERT] = "Insert",
[ui.SDLK_HOME] = "Home",
[ui.SDLK_PAGEUP] = "PageUp",
[ui.SDLK_DELETE] = "Delete",
[ui.SDLK_END] = "End",
[ui.SDLK_PAGEDOWN] = "PageDown",
[ui.SDLK_RIGHT] = "Right",
[ui.SDLK_LEFT] = "Left",
[ui.SDLK_DOWN] = "Down",
[ui.SDLK_UP] = "Up",
[ui.SDLK_LEFTBRACKET] = "[",
[ui.SDLK_RIGHTBRACKET] = "]",
[ui.SDLK_BACKSLASH] = "\\",
[ui.SDLK_SEMICOLON] = ";",
[ui.SDLK_QUOTE] = "'",
[ui.SDLK_COMMA] = ",",
[ui.SDLK_PERIOD] = ".",
[ui.SDLK_SLASH] = "/",
[ui.SDLK_MINUS] = "-",
[ui.SDLK_EQUALS] = "=",
[ui.SDLK_BACKQUOTE] = "`",
[ui.SDLK_LCTRL] = "Left Ctrl",
[ui.SDLK_RCTRL] = "Right Ctrl",
[ui.SDLK_LSHIFT] = "Left Shift",
[ui.SDLK_RSHIFT] = "Right Shift",
[ui.SDLK_LALT] = "Left Alt",
[ui.SDLK_RALT] = "Right Alt",
[ui.SDLK_KP_ENTER] = "Numpad Enter",
[ui.SDLK_KP_PLUS] = "Numpad +",
[ui.SDLK_KP_MINUS] = "Numpad -",
[ui.SDLK_KP_DIVIDE] = "Numpad /",
[ui.SDLK_KP_MULTIPLY] = "Numpad *",
[ui.SDLK_KP_PERIOD] = "Numpad .",
[ui.SDLK_KP_0] = "Numpad 0", [ui.SDLK_KP_1] = "Numpad 1", [ui.SDLK_KP_2] = "Numpad 2",
[ui.SDLK_KP_3] = "Numpad 3", [ui.SDLK_KP_4] = "Numpad 4", [ui.SDLK_KP_5] = "Numpad 5",
[ui.SDLK_KP_6] = "Numpad 6", [ui.SDLK_KP_7] = "Numpad 7", [ui.SDLK_KP_8] = "Numpad 8",
[ui.SDLK_KP_9] = "Numpad 9",
}
local MODIFIER_KEYS = {
[ui.SDLK_LCTRL] = true, [ui.SDLK_RCTRL] = true,
[ui.SDLK_LSHIFT] = true, [ui.SDLK_RSHIFT] = true,
[ui.SDLK_LALT] = true, [ui.SDLK_RALT] = true,
}
local function keyName(kc)
if kc == nil then return "unbound" end
local special = SPECIAL_KEY_NAMES[kc]
if special then return special end
if kc >= 32 and kc < 127 then
local ch = string.char(kc)
if ch ~= " " then return ch:upper() end
end
return "key#" .. tostring(kc)
end
local function defaultBinds()
local b = {}
for _, a in ipairs(BIND_ACTIONS) do b[a.name] = a.default end
return b
end
local SETTINGS_PATH = "line_tool_plus_settings.txt"
local MANAGER_ID = "line_tool_plus"
local useManager = (MANAGER ~= nil and MANAGER.savesetting ~= nil and MANAGER.getsetting ~= nil)
local SETTING_KEYS = {"dock", "snap", "category", "mode", "grid", "fill", "repeat", "binds", "mirror", "mirror_axis", "force_mobile"}
local state = {
active = false,
phase = "EMPTY",
category = DEFAULT_CATEGORY,
mode = DEFAULT_MODE,
A = nil, B = nil,
H1 = nil, H2 = nil,
arcAngle = DEFAULT_ARC_ANGLE,
points = {},
origin = nil,
radiusPt = nil,
drag = nil,
shiftDown = false,
showMenu = false,
menuRects = {},
menuTab = "general",
bindPage = 1,
rebindAction = nil,
pendingConflict = nil,
dock = DEFAULT_DOCK,
snapIdx = DEFAULT_SNAP_IDX,
grid = DEFAULT_GRID,
fill = DEFAULT_FILL,
repeatOn = DEFAULT_REPEAT,
binds = defaultBinds(),
mirror = DEFAULT_MIRROR,
mirrorAxis = DEFAULT_MIRROR_AXIS,
mobileRects = {},
flipState = 0,
forceMobile = "auto",
isMobile = MOBILE_AUTO,
currentTick = 0,
textInputDeadlineTick = 0,
}
local function recomputeMobile()
if state.forceMobile == "on" then
state.isMobile = true
elseif state.forceMobile == "off" then
state.isMobile = false
else
state.isMobile = MOBILE_AUTO
end
end
local handleMenuClick, drawMenu, drawGeneralTab, drawKeybindsTab, drawHud, dockOrigin, resetPoints
local function snapVal() return SNAP_VALUES[state.snapIdx] end
local function isPoly() return state.mode == "CUSTOM" end
local function isClosedShape() return state.mode == "CUSTOM" and state.category == "Shape" end
local function isPreset() return SHAPE_DEFS[state.mode] ~= nil end
local function canFill() return isPreset() or isClosedShape() end
local function gridSnap(x, y)
if not state.grid then return x, y end
local g = GRID_SIZE
return floor((x + g / 2) / g) * g, floor((y + g / 2) / g) * g
end
local function indexOf(list, val)
for i, v in ipairs(list) do if v == val then return i end end
return nil
end
local function serializeBinds()
local parts = {}
for _, a in ipairs(BIND_ACTIONS) do
local kc = state.binds[a.name]
parts[#parts + 1] = a.name .. "=" .. (kc and tostring(kc) or "none")
end
return table.concat(parts, "|")
end
local function deserializeBinds(s)
if not s or s == "" then return end
local sawFlipCycle = false
for entry in s:gmatch("([^|]+)") do
local name, val = entry:match("^([%w_]+)=(.+)$")
if name and val and BIND_LOOKUP[name] then
if name == "flip_cycle" then sawFlipCycle = true end
if val == "none" then
state.binds[name] = nil
else
local n = tonumber(val)
if n then
n = floor(n)
if MODIFIER_KEYS[n] then
state.binds[name] = BIND_LOOKUP[name].default
else
state.binds[name] = n
end
end
end
end
end
if not sawFlipCycle then
state.binds.toggle_fill = ui.SDLK_v
state.binds.flip_cycle = ui.SDLK_f
end
end
local function applySetting(k, v)
if k == "dock" and indexOf(DOCK_POSITIONS, v) then
state.dock = v
elseif k == "snap" then
local n = tonumber(v)
local idx = n and indexOf(SNAP_VALUES, n)
if idx then state.snapIdx = idx end
elseif k == "category" and indexOf(CATEGORIES, v) then
state.category = v
elseif k == "mode" then
local modes = CATEGORY_MODES[state.category]
if modes and indexOf(modes, v) then state.mode = v end
elseif k == "grid" then
state.grid = (v == "true" or v == "1")
elseif k == "fill" then
state.fill = (v == "true" or v == "1")
elseif k == "repeat" then
state.repeatOn = (v == "true" or v == "1")
elseif k == "binds" then
deserializeBinds(v)
elseif k == "mirror" then
state.mirror = (v == "true" or v == "1")
elseif k == "mirror_axis" then
if v == "H" or v == "V" or v == "HV" or v == "DIAG" then state.mirrorAxis = v end
elseif k == "force_mobile" then
if v == "auto" or v == "on" or v == "off" then
state.forceMobile = v
recomputeMobile()
end
end
end
local function currentSettings()
return {
dock = state.dock,
snap = tostring(snapVal()),
category = state.category,
mode = state.mode,
grid = tostring(state.grid),
fill = tostring(state.fill),
["repeat"] = tostring(state.repeatOn),
binds = serializeBinds(),
mirror = tostring(state.mirror),
mirror_axis = state.mirrorAxis,
force_mobile = state.forceMobile,
}
end
local function loadSettings()
if useManager then
for _, k in ipairs(SETTING_KEYS) do
local v = MANAGER.getsetting(MANAGER_ID, k)
if v then applySetting(k, v) end
end
return
end
local f = io.open(SETTINGS_PATH, "r")
if not f then return end
for line in f:lines() do
local k, v = line:match("^%s*(%w+)%s*=%s*(.+)%s*$")
if k then applySetting(k, v) end
end
f:close()
end
local function saveSettings()
local values = currentSettings()
if useManager then
for _, k in ipairs(SETTING_KEYS) do
MANAGER.savesetting(MANAGER_ID, k, values[k])
end
return
end
local f = io.open(SETTINGS_PATH, "w")
if not f then return end
for _, k in ipairs(SETTING_KEYS) do
f:write(k .. "=" .. values[k] .. "\n")
end
f:close()
end
local function resetToDefaults()
state.dock = DEFAULT_DOCK
state.snapIdx = DEFAULT_SNAP_IDX
state.category = DEFAULT_CATEGORY
state.mode = DEFAULT_MODE
state.grid = DEFAULT_GRID
state.fill = DEFAULT_FILL
state.repeatOn = DEFAULT_REPEAT
state.mirror = DEFAULT_MIRROR
state.mirrorAxis = DEFAULT_MIRROR_AXIS
state.forceMobile = "auto"
recomputeMobile()
saveSettings()
end
loadSettings()
local function dist(p1, p2)
local dx, dy = p2.x - p1.x, p2.y - p1.y
return sqrt(dx * dx + dy * dy)
end
local function hitTest(mx, my, hx, hy)
local dx, dy = mx - hx, my - hy
return (dx * dx + dy * dy) <= GRAB_R2
end
local function snapDeg(degVal, step)
return floor((degVal + step / 2) / step) * step
end
local function snapFromAnchor(anchor, x, y)
local ddx, ddy = x - anchor.x, y - anchor.y
local distA = sqrt(ddx * ddx + ddy * ddy)
local angle = deg(atan2(ddy, ddx))
local snapped = rad(snapDeg(angle, snapVal()))
return anchor.x + distA * cos(snapped), anchor.y + distA * sin(snapped)
end
local function zoomInfo()
if not (renderer ~= nil and renderer.zoomEnabled and renderer.zoomEnabled()) then
return nil
end
local winX, winY, factor = renderer.zoomWindow()
local scopeX, scopeY, size = renderer.zoomScope()
if not (winX and winY and factor and scopeX and scopeY and size) then
return nil
end
return {
winX = winX, winY = winY, factor = factor,
scopeX = scopeX, scopeY = scopeY, size = size,
winW = size * factor, winH = size * factor,
}
end
local function remapZoomCoords(x, y)
local zi = zoomInfo()
if not zi then return x, y end
if x >= zi.winX and x < zi.winX + zi.winW and y >= zi.winY and y < zi.winY + zi.winH then
local simX = (x - zi.winX) / zi.factor + zi.scopeX
local simY = (y - zi.winY) / zi.factor + zi.scopeY
return simX, simY
end
return x, y
end
local function zDrawPoint(p, col, size)
local zi = zoomInfo()
if not zi then return end
if p.x < zi.scopeX or p.x >= zi.scopeX + zi.size or p.y < zi.scopeY or p.y >= zi.scopeY + zi.size then
return
end
local sx = zi.winX + (p.x - zi.scopeX) * zi.factor
local sy = zi.winY + (p.y - zi.scopeY) * zi.factor
local sSize = max(1, floor(size * zi.factor))
local half = floor(sSize / 2)
graphics.fillRect(floor(sx - half), floor(sy - half), sSize, sSize, col[1], col[2], col[3], col[4])
end
local function zDrawLine(x1, y1, x2, y2, r, g, b, a)
local zi = zoomInfo()
if not zi then return end
if x1 < zi.scopeX or x1 >= zi.scopeX + zi.size or y1 < zi.scopeY or y1 >= zi.scopeY + zi.size then
return
end
if x2 < zi.scopeX or x2 >= zi.scopeX + zi.size or y2 < zi.scopeY or y2 >= zi.scopeY + zi.size then
return
end
local sx1 = zi.winX + (x1 - zi.scopeX) * zi.factor
local sy1 = zi.winY + (y1 - zi.scopeY) * zi.factor
local sx2 = zi.winX + (x2 - zi.scopeX) * zi.factor
local sy2 = zi.winY + (y2 - zi.scopeY) * zi.factor
graphics.drawLine(floor(sx1), floor(sy1), floor(sx2), floor(sy2), r, g, b, a)
end
local MIRROR_AXES = {"H", "V", "HV", "DIAG"}
local function mirrorCenter()
return sim.XRES / 2, sim.YRES / 2
end
local function mirrorTwins(p)
if not state.mirror then return nil end
local cx, cy = mirrorCenter()
if state.mirrorAxis == "V" then
return {{x = 2 * cx - p.x, y = p.y}}
elseif state.mirrorAxis == "H" then
return {{x = p.x, y = 2 * cy - p.y}}
elseif state.mirrorAxis == "HV" then
return {
{x = 2 * cx - p.x, y = p.y},
{x = p.x, y = 2 * cy - p.y},
{x = 2 * cx - p.x, y = 2 * cy - p.y},
}
elseif state.mirrorAxis == "DIAG" then
return {{x = cx + (p.y - cy), y = cy + (p.x - cx)}}
end
return nil
end
local function drawDashedLine(x1, y1, x2, y2, r, g, b, a, zoom)
local dxL, dyL = x2 - x1, y2 - y1
local len = sqrt(dxL * dxL + dyL * dyL)
if len < 1 then return end
local ux, uy = dxL / len, dyL / len
local seg = 4
local i = 0
while i < len do
local sx = x1 + ux * i
local sy = y1 + uy * i
local ex = x1 + ux * min(i + seg, len)
local ey = y1 + uy * min(i + seg, len)
if zoom then
zDrawLine(sx, sy, ex, ey, r, g, b, a)
else
graphics.drawLine(floor(sx), floor(sy), floor(ex), floor(ey), r, g, b, a)
end
i = i + seg * 2
end
end
local function drawMirrorAxis(zoom)
if not state.mirror then return end
local cx, cy = mirrorCenter()
local W, H = sim.XRES - 1, sim.YRES - 1
local col = {180, 120, 220, 110}
if state.mirrorAxis == "H" then
drawDashedLine(0, cy, W, cy, col[1], col[2], col[3], col[4], zoom)
elseif state.mirrorAxis == "V" then
drawDashedLine(cx, 0, cx, H, col[1], col[2], col[3], col[4], zoom)
elseif state.mirrorAxis == "HV" then
drawDashedLine(cx, 0, cx, H, col[1], col[2], col[3], col[4], zoom)
drawDashedLine(0, cy, W, cy, col[1], col[2], col[3], col[4], zoom)
elseif state.mirrorAxis == "DIAG" then
local d = min(cx, cy)
drawDashedLine(cx - d, cy - d, cx + d, cy + d, col[1], col[2], col[3], col[4], zoom)
end
end
local function quadPoint(t, a, h, b)
local mt = 1 - t
local x = mt * mt * a.x + 2 * mt * t * h.x + t * t * b.x
local y = mt * mt * a.y + 2 * mt * t * h.y + t * t * b.y
return x, y
end
local function cubicPoint(t, a, h1, h2, b)
local mt = 1 - t
local mt2, t2 = mt * mt, t * t
local x = mt2 * mt * a.x + 3 * mt2 * t * h1.x + 3 * mt * t2 * h2.x + t2 * t * b.x
local y = mt2 * mt * a.y + 3 * mt2 * t * h1.y + 3 * mt * t2 * h2.y + t2 * t * b.y
return x, y
end
local function arcParams(a, b, angleDeg)
local dx, dy = b.x - a.x, b.y - a.y
local chord = sqrt(dx * dx + dy * dy)
if chord < 0.0001 or abs(angleDeg) < 0.0001 then
return nil
end
local thetaRad = rad(angleDeg)
local half = thetaRad / 2
local r = (chord / 2) / sin(half)
local h = r * cos(half)
local perpx, perpy = -dy / chord, dx / chord
local mx, my = (a.x + b.x) / 2, (a.y + b.y) / 2
local cx, cy = mx + h * perpx, my + h * perpy
local a0 = atan2(a.y - cy, a.x - cx)
return cx, cy, abs(r), a0, thetaRad
end
local function arcPoint(t, a, b, angleDeg)
local cx, cy, r, a0, thetaRad = arcParams(a, b, angleDeg)
if not cx then
return a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t
end
local ang = a0 + t * thetaRad
return cx + r * cos(ang), cy + r * sin(ang)
end
local function curvePoint(t)
if state.mode == "QUAD" then
return quadPoint(t, state.A, state.H1, state.B)
elseif state.mode == "CUBIC" then
return cubicPoint(t, state.A, state.H1, state.H2, state.B)
else
return arcPoint(t, state.A, state.B, state.arcAngle)
end
end
local function chordEstimate()
if state.mode == "QUAD" then
return dist(state.A, state.H1) + dist(state.H1, state.B)
elseif state.mode == "CUBIC" then
return dist(state.A, state.H1) + dist(state.H1, state.H2) + dist(state.H2, state.B)
else
local cx, cy, r, a0, thetaRad = arcParams(state.A, state.B, state.arcAngle)
if not cx then return dist(state.A, state.B) end
return r * abs(thetaRad)
end
end
local function sampleCount(pxPerStep)
local len = chordEstimate()
return max(2, ceil(len / pxPerStep))
end
local function initHandles()
if not (state.A and state.B) then return end
local a, b = state.A, state.B
if state.mode == "QUAD" then
state.H1 = {x = (a.x + b.x) / 2, y = (a.y + b.y) / 2}
state.H2 = nil
elseif state.mode == "CUBIC" then
state.H1 = {x = a.x + (b.x - a.x) / 3, y = a.y + (b.y - a.y) / 3}
state.H2 = {x = a.x + (b.x - a.x) * 2 / 3, y = a.y + (b.y - a.y) * 2 / 3}
else
state.H1, state.H2 = nil, nil
state.arcAngle = DEFAULT_ARC_ANGLE
end
end
function resetPoints()
state.A, state.B, state.H1, state.H2 = nil, nil, nil, nil
state.points = {}
state.origin, state.radiusPt = nil, nil
state.drag = nil
state.phase = "EMPTY"
state.flipState = 0
end
local function activePoints()
if isPoly() then return state.points end
if isPreset() then
local out = {}
if state.origin then out[#out + 1] = state.origin end
if state.radiusPt then out[#out + 1] = state.radiusPt end
return out
end
local out = {}
if state.A then out[#out + 1] = state.A end
if state.B then out[#out + 1] = state.B end
if state.H1 then out[#out + 1] = state.H1 end
if state.H2 then out[#out + 1] = state.H2 end
return out
end
local function flipInPlace(axis)
local pts = activePoints()
if #pts == 0 then return end
local cx, cy = 0, 0
for _, p in ipairs(pts) do cx = cx + p.x; cy = cy + p.y end
cx = cx / #pts; cy = cy / #pts
for _, p in ipairs(pts) do
if axis == "V" or axis == "HV" then p.x = 2 * cx - p.x end
if axis == "H" or axis == "HV" then p.y = 2 * cy - p.y end
end
end
local FLIP_DELTA = { [0] = "V", [1] = "HV", [2] = "V", [3] = "HV" }
local function cycleFlip()
local pts = activePoints()
if #pts == 0 then return end
local s = state.flipState or 0
flipInPlace(FLIP_DELTA[s])
state.flipState = (s + 1) % 4
end
local function shapeVertices()
if not (state.origin and state.radiusPt) then return nil end
local def = SHAPE_DEFS[state.mode]
if not def then return nil end
local ox, oy = state.origin.x, state.origin.y
local dx, dy = state.radiusPt.x - ox, state.radiusPt.y - oy
local R = sqrt(dx * dx + dy * dy)
if R < 0.5 then return nil end
local baseAng = atan2(dy, dx)
local offset = rad(def.angleOffset or 0)
local verts = {}
if def.inner then
local step = math.pi / def.n
local innerR = R * def.inner
for i = 0, def.n * 2 - 1 do
local ang = baseAng + offset + i * step
local r = (i % 2 == 0) and R or innerR
verts[#verts + 1] = {x = ox + r * cos(ang), y = oy + r * sin(ang)}
end
else
local step = (2 * math.pi) / def.n
for i = 0, def.n - 1 do
local ang = baseAng + offset + i * step
verts[#verts + 1] = {x = ox + R * cos(ang), y = oy + R * sin(ang)}
end
end
return verts
end
local function toElementId(v)
if type(v) == "number" then return v end
if type(v) ~= "string" then return 0 end
if elements and elements[v] then return elements[v] end
if tpt.element then
local short = v:match("_PT_(.+)$") or v
local ok, id = pcall(tpt.element, short)
if ok and id then return id end
end
return 0
end
local function selectedElemName()
local sel = tpt.selectedl
if type(sel) == "string" and #sel > 0 then
local short = sel:match("_PT_(.+)$") or sel
return short
end
if type(sel) == "number" and elements and elements.property then
local ok, name = pcall(elements.property, sel, "Name")
if ok and type(name) == "string" and #name > 0 then return name end
end
return "?"
end
local function rotatePreset(degDelta)
if not (state.origin and state.radiusPt) then return end
local ox, oy = state.origin.x, state.origin.y
local dx, dy = state.radiusPt.x - ox, state.radiusPt.y - oy
local ang = atan2(dy, dx) + rad(degDelta)
local r = sqrt(dx * dx + dy * dy)
state.radiusPt.x = ox + r * cos(ang)
state.radiusPt.y = oy + r * sin(ang)
end
local function stampLine(x1, y1, x2, y2, sel, rx, ry, shape)
local ix1, iy1 = floor(x1 + 0.5), floor(y1 + 0.5)
local ix2, iy2 = floor(x2 + 0.5), floor(y2 + 0.5)
if ix1 == ix2 and iy1 == iy2 then return end
sim.createLine(ix1, iy1, ix2, iy2, rx, ry, sel, shape)
end
local function fillPolygon(verts, sel)
if not verts or #verts < 3 then return end
local minY, maxY = verts[1].y, verts[1].y
for _, v in ipairs(verts) do
if v.y < minY then minY = v.y end
if v.y > maxY then maxY = v.y end
end
minY = max(0, floor(minY))
maxY = min(sim.YRES - 1, ceil(maxY))
for y = minY, maxY do
local xs = {}
for i = 1, #verts do
local a = verts[i]
local b = verts[(i % #verts) + 1]
local ay, by = a.y, b.y
if (ay <= y and by > y) or (by <= y and ay > y) then
local t = (y - ay) / (by - ay)
xs[#xs + 1] = a.x + t * (b.x - a.x)
end
end
table.sort(xs)
for i = 1, #xs - 1, 2 do
local x1 = max(0, floor(xs[i] + 0.5))
local x2 = min(sim.XRES - 1, floor(xs[i + 1] + 0.5))
if x2 >= x1 then
sim.createLine(x1, y, x2, y, 0, 0, sel, 0)
end
end
end
end
local function stampLineMirrored(x1, y1, x2, y2, sel, rx, ry, shape)
stampLine(x1, y1, x2, y2, sel, rx, ry, shape)
local twinsA = mirrorTwins({x = x1, y = y1})
local twinsB = mirrorTwins({x = x2, y = y2})
if not (twinsA and twinsB) then return end
for i, ta in ipairs(twinsA) do
local tb = twinsB[i]
stampLine(ta.x, ta.y, tb.x, tb.y, sel, rx, ry, shape)
end
end
local function fillPolygonMirrored(verts, sel)
fillPolygon(verts, sel)
if not state.mirror then return end
local axes = {}
if state.mirrorAxis == "HV" then axes = {"H", "V", "HV"}
elseif state.mirrorAxis == "H" then axes = {"H"}
elseif state.mirrorAxis == "V" then axes = {"V"}
elseif state.mirrorAxis == "DIAG" then axes = {"DIAG"} end
local cx, cy = mirrorCenter()
for _, ax in ipairs(axes) do
local twin = {}
for _, v in ipairs(verts) do
if ax == "H" then twin[#twin + 1] = {x = 2 * cx - v.x, y = v.y}
elseif ax == "V" then twin[#twin + 1] = {x = v.x, y = 2 * cy - v.y}
elseif ax == "HV" then twin[#twin + 1] = {x = 2 * cx - v.x, y = 2 * cy - v.y}
elseif ax == "DIAG" then twin[#twin + 1] = {x = cx + (v.y - cy), y = cy + (v.x - cx)} end
end
fillPolygon(twin, sel)
end
end
local function commit()
local ok, err = pcall(function()
local sel = toElementId(tpt.selectedl)
local rx, ry = tpt.brushx, tpt.brushy
local shape = tpt.brushID or 0
if isPreset() then
local verts = shapeVertices()
if not verts or #verts < 2 then return end
if state.fill then fillPolygonMirrored(verts, sel) end
for i = 1, #verts do
local a = verts[i]
local b = verts[(i % #verts) + 1]
stampLineMirrored(a.x, a.y, b.x, b.y, sel, rx, ry, shape)
end
elseif isPoly() then
local pts = state.points
if #pts < 2 then return end
if state.fill and isClosedShape() and #pts >= 3 then
fillPolygonMirrored(pts, sel)
end
for i = 1, #pts - 1 do
local a, b = pts[i], pts[i + 1]
stampLineMirrored(a.x, a.y, b.x, b.y, sel, rx, ry, shape)
end
if isClosedShape() and #pts >= 3 then
local a, b = pts[#pts], pts[1]
stampLineMirrored(a.x, a.y, b.x, b.y, sel, rx, ry, shape)
end
else
local steps = sampleCount(0.5)
local px, py = curvePoint(0)
for i = 1, steps do
local t = i / steps
local cx, cy = curvePoint(t)
stampLineMirrored(px, py, cx, cy, sel, rx, ry, shape)
px, py = cx, cy
end
end
end)
if not ok then
print("Line Tool+: commit error - " .. tostring(err))
end
if state.repeatOn then
state.drag = nil
else
resetPoints()
end
end
local function setMode(name)
local modes = CATEGORY_MODES[state.category]
if not (modes and indexOf(modes, name)) then return end
state.mode = name
resetPoints()
saveSettings()
end
local function cycleMode(delta)
local modes = CATEGORY_MODES[state.category]
local idx = indexOf(modes, state.mode) or 1
idx = ((idx - 1 + delta) % #modes) + 1
state.mode = modes[idx]
resetPoints()
saveSettings()
end
local function setCategory(name)
if not indexOf(CATEGORIES, name) then return end
state.category = name
local modes = CATEGORY_MODES[name]
if not indexOf(modes, state.mode) then state.mode = modes[1] end
resetPoints()
saveSettings()
end
local function cycleCategory()
local idx = indexOf(CATEGORIES, state.category) or 1
idx = (idx % #CATEGORIES) + 1
setCategory(CATEGORIES[idx])
end
local function toggleClosed()
if state.mode == "CUSTOM" then
state.category = (state.category == "Line") and "Shape" or "Line"
else
state.category = "Line"; state.mode = "CUSTOM"
end
resetPoints()
saveSettings()
end
local TEXT_INPUT_DECAY_TICKS = 60
event.register(evt.textinput, function()
state.textInputDeadlineTick = state.currentTick + TEXT_INPUT_DECAY_TICKS
end)
event.register(evt.textediting, function()
state.textInputDeadlineTick = state.currentTick + TEXT_INPUT_DECAY_TICKS
end)
event.register(evt.keypress, function(key, scan, rep, shift, ctrl, alt)
if state.currentTick < state.textInputDeadlineTick then
return
end
state.shiftDown = shift
if state.rebindAction then
if rep then return false end
if key == K_ESCAPE then
state.rebindAction = nil
return false
end
local actingOn = state.rebindAction
if MODIFIER_KEYS[key] then
state.pendingConflict = {action = actingOn, key = key, reason = "modifier"}
state.rebindAction = nil
return false
end
local conflictWith = nil
for _, a in ipairs(BIND_ACTIONS) do
if a.name ~= actingOn and state.binds[a.name] == key then
conflictWith = a.name
break
end
end
if conflictWith then
state.pendingConflict = {action = actingOn, key = key, other = conflictWith}
state.rebindAction = nil
else
state.binds[actingOn] = key
state.rebindAction = nil
saveSettings()
end
return false
end
if state.pendingConflict then
if not rep and key == K_ESCAPE then
state.pendingConflict = nil
end
return false
end
if key == state.binds.toggle and not ctrl and not alt then
if rep then return false end
if state.active then
state.active = false
state.showMenu = false
resetPoints()
else
state.active = true
resetPoints()
end
return false
end
if not state.active then return end
if ctrl or alt then return end
if state.showMenu then
if key == K_ESCAPE or key == state.binds.settings then
if rep then return false end
state.showMenu = false
return false
end
return false
end
if key == state.binds.settings then
if rep then return false end
state.showMenu = true
return false
end
if key == state.binds.cycle_category then
if rep then return false end
cycleCategory()
return false
end
if key == state.binds.toggle_closed then
if rep then return false end
toggleClosed()
return false
end
if key == state.binds.toggle_grid then
if rep then return false end
state.grid = not state.grid
saveSettings()
return false
end
if key == state.binds.toggle_fill then
if rep then return false end
state.fill = not state.fill
saveSettings()
return false
end
if key == state.binds.toggle_repeat then
if rep then return false end
state.repeatOn = not state.repeatOn
saveSettings()
return false
end
if key == state.binds.cycle_mode then
if rep then return false end
cycleMode(1)
return false
end
local modes = CATEGORY_MODES[state.category]
if key == state.binds.select_mode_1 and modes[1] then setMode(modes[1]); return false end
if key == state.binds.select_mode_2 and modes[2] then setMode(modes[2]); return false end
if key == state.binds.select_mode_3 and modes[3] then setMode(modes[3]); return false end
if key == state.binds.select_mode_4 and modes[4] then setMode(modes[4]); return false end
if key == state.binds.select_mode_5 and modes[5] then setMode(modes[5]); return false end
if key == state.binds.select_mode_6 and modes[6] then setMode(modes[6]); return false end
if key == state.binds.select_mode_7 and modes[7] then setMode(modes[7]); return false end
if key == state.binds.select_mode_8 and modes[8] then setMode(modes[8]); return false end
if key == K_ESCAPE then
if rep then return false end
if isPoly() then
if #state.points > 0 then resetPoints() end
elseif isPreset() then
if state.phase == "NEED_RADIUS" then
state.origin = nil
state.phase = "EMPTY"
elseif state.phase == "ADJUST" then
resetPoints()
end
else
if state.phase == "NEED_B" then
state.A = nil
state.phase = "EMPTY"
elseif state.phase == "ADJUST" then
resetPoints()
end
end
return false
end
if key == state.binds.remove_point and isPoly() then
if rep then return false end
if #state.points > 0 then
state.points[#state.points] = nil
if #state.points == 0 then state.phase = "EMPTY" end
end
return false
end
if key == state.binds.commit then
if rep then return false end
local didCommit = false
if isPoly() then
if #state.points >= 2 then commit(); didCommit = true end
elseif isPreset() then
if state.phase == "ADJUST" then commit(); didCommit = true end
elseif state.phase == "ADJUST" then
commit()
didCommit = true
end
if didCommit then return false end
return
elseif state.phase == "ADJUST" and state.mode == "ARC" then
if key == state.binds.arc_angle_inc then
state.arcAngle = state.arcAngle + 5
return false
elseif key == state.binds.arc_angle_dec then
state.arcAngle = state.arcAngle - 5
return false
elseif key == state.binds.fine_inc then
state.arcAngle = state.arcAngle + 1
return false
elseif key == state.binds.fine_dec then
state.arcAngle = state.arcAngle - 1
return false
end
elseif state.phase == "ADJUST" and isPreset() then
if key == state.binds.rotate_cw then
rotatePreset(5); return false
elseif key == state.binds.rotate_ccw then
rotatePreset(-5); return false
elseif key == state.binds.fine_inc then
rotatePreset(1); return false
elseif key == state.binds.fine_dec then
rotatePreset(-1); return false
end
end
if key == state.binds.flip_cycle then if not rep then cycleFlip() end; return false end
if key == state.binds.flip_h then if not rep then flipInPlace("H"); state.flipState = 0 end; return false end
if key == state.binds.flip_v then if not rep then flipInPlace("V"); state.flipState = 0 end; return false end
if key == state.binds.flip_hv then if not rep then flipInPlace("HV"); state.flipState = 0 end; return false end
if key == state.binds.toggle_mirror then
if not rep then state.mirror = not state.mirror; saveSettings() end
return false
end
if key == state.binds.cycle_mirror_axis then
if not rep then
local i = indexOf(MIRROR_AXES, state.mirrorAxis) or 1
state.mirrorAxis = MIRROR_AXES[(i % #MIRROR_AXES) + 1]
saveSettings()
end
return false
end
end)
event.register(evt.keyrelease, function(key, scan, rep, shift, ctrl, alt)
state.shiftDown = shift
end)
local function hitPolyPoint(x, y)
for i, p in ipairs(state.points) do
if hitTest(x, y, p.x, p.y) then return i end
end
return nil
end
event.register(evt.mousedown, function(x, y, button)
if not state.active then
if state.isMobile and not state.showMenu and button == 1 then
for _, r in ipairs(state.mobileRects) do
if x >= r.x1 and x <= r.x2 and y >= r.y1 and y <= r.y2 then
if r.action == "activate" then
state.active = true
resetPoints()
end
return false
end
end
end
return
end
if state.isMobile and not state.showMenu and button == 1 then
for _, r in ipairs(state.mobileRects) do
if x >= r.x1 and x <= r.x2 and y >= r.y1 and y <= r.y2 then
if r.action == "menu" then state.showMenu = true
elseif r.action == "cycle" then cycleMode(1)
elseif r.action == "flip" then cycleFlip()
elseif r.action == "commit" then
if isPoly() and #state.points >= 2 then commit()
elseif isPreset() and state.phase == "ADJUST" then commit()
elseif state.phase == "ADJUST" then commit()
end
elseif r.action == "undo" then
if isPoly() and #state.points > 0 then
state.points[#state.points] = nil
if #state.points == 0 then state.phase = "EMPTY" end
end
elseif r.action == "cancel" then
resetPoints()
elseif r.action == "off" then
state.active = false
state.showMenu = false
resetPoints()
end
return false
end
end
end
x, y = remapZoomCoords(x, y)
if state.showMenu then
if button == 1 then handleMenuClick(x, y) end
return false
end
if x < 0 or x >= sim.XRES or y < 0 or y >= sim.YRES then
return
end
if isPreset() then
if button == 1 then
if state.phase == "EMPTY" then
local ox, oy = gridSnap(x, y)
state.origin = {x = ox, y = oy}
state.phase = "NEED_RADIUS"
return false
elseif state.phase == "NEED_RADIUS" then
local rx2, ry2 = x, y
if state.shiftDown and state.origin then
rx2, ry2 = snapFromAnchor(state.origin, x, y)
end
rx2, ry2 = gridSnap(rx2, ry2)
state.radiusPt = {x = rx2, y = ry2}
state.phase = "ADJUST"
return false
elseif state.phase == "ADJUST" then
if state.radiusPt and hitTest(x, y, state.radiusPt.x, state.radiusPt.y) then
state.drag = "R"
elseif state.origin and hitTest(x, y, state.origin.x, state.origin.y) then
state.drag = "O"
elseif state.shiftDown then
state.drag = "TRANSLATE"
state.translateStart = {x = x, y = y}
end
return false
end
elseif button == 3 then
if state.phase == "ADJUST" then commit() end
return false
end
return false
end
if isPoly() then
if button == 1 then
local hit = hitPolyPoint(x, y)
if hit then
state.drag = "P" .. hit
elseif state.shiftDown and #state.points > 0 then
state.drag = "MAYBE_TRANSLATE_OR_ADD"
state.dragStart = {x = x, y = y}
else
local px, py = gridSnap(x, y)
state.points[#state.points + 1] = {x = px, y = py}
state.phase = "COLLECT"
end
return false
elseif button == 3 then
if #state.points >= 2 then commit() end
return false
end
return false
end
if button == 1 then
if state.phase == "EMPTY" then
local ax, ay = gridSnap(x, y)
state.A = {x = ax, y = ay}
state.phase = "NEED_B"
state.drag = "PLACING_B"
state.dragStart = {x = x, y = y}
state.B = {x = ax, y = ay}
return false
elseif state.phase == "NEED_B" then
local bx, by = gridSnap(x, y)
state.B = {x = bx, y = by}
state.phase = "ADJUST"
initHandles()
return false
elseif state.phase == "ADJUST" then
if state.H1 and hitTest(x, y, state.H1.x, state.H1.y) then
state.drag = "H1"
elseif state.H2 and hitTest(x, y, state.H2.x, state.H2.y) then
state.drag = "H2"
elseif state.A and hitTest(x, y, state.A.x, state.A.y) then
state.drag = "A"
elseif state.B and hitTest(x, y, state.B.x, state.B.y) then
state.drag = "B"
elseif state.shiftDown then
state.drag = "TRANSLATE"
state.translateStart = {x = x, y = y}
end
return false
end
elseif button == 3 then
if state.phase == "ADJUST" then
commit()
return false
end
end
end)
event.register(evt.mouseup, function(x, y, button, reason)
if state.active then
x, y = remapZoomCoords(x, y)
local drag = state.drag
if drag == "PLACING_B" and button == 1 then
if state.B and state.dragStart and dist(state.dragStart, {x = x, y = y}) >= 3 then
state.phase = "ADJUST"
initHandles()
else
state.B = nil
state.phase = "NEED_B"
end
elseif drag == "MAYBE_TRANSLATE_OR_ADD" then
local px, py = x, y
if state.shiftDown and #state.points > 0 then
local last = state.points[#state.points]
px, py = snapFromAnchor(last, x, y)
end
px, py = gridSnap(px, py)
state.points[#state.points + 1] = {x = px, y = py}
state.phase = "COLLECT"
end
end
state.drag = nil
end)
local cursor = {x = -1, y = -1, inSim = false}
event.register(evt.mousemove, function(x, y, dx, dy)
local prevX, prevY = cursor.x, cursor.y
x, y = remapZoomCoords(x, y)
cursor.x, cursor.y = x, y
cursor.inSim = (x >= 0 and x < sim.XRES and y >= 0 and y < sim.YRES)
if not (state.active and state.drag) then return end
local drag = state.drag
local rdx = (prevX and (x - prevX)) or dx
local rdy = (prevY and (y - prevY)) or dy
local hx, hy = x, y
if state.shiftDown and (drag == "H1" or drag == "H2") and state.A then
hx, hy = snapFromAnchor(state.A, x, y)
end
hx, hy = gridSnap(hx, hy)
local sx, sy = gridSnap(x, y)
if drag == "H1" then
state.H1.x, state.H1.y = hx, hy
elseif drag == "H2" then
state.H2.x, state.H2.y = hx, hy
elseif drag == "A" then
state.A.x, state.A.y = sx, sy
elseif drag == "B" then
state.B.x, state.B.y = sx, sy
elseif drag == "O" then
if state.origin then
local ddx = sx - state.origin.x
local ddy = sy - state.origin.y
state.origin.x, state.origin.y = sx, sy
if state.radiusPt then
state.radiusPt.x = state.radiusPt.x + ddx
state.radiusPt.y = state.radiusPt.y + ddy
end
end
elseif drag == "R" then
local rx2, ry2 = x, y
if state.shiftDown and state.origin then
rx2, ry2 = snapFromAnchor(state.origin, x, y)
end
rx2, ry2 = gridSnap(rx2, ry2)
if state.radiusPt then
state.radiusPt.x, state.radiusPt.y = rx2, ry2
end
elseif drag == "PLACING_B" then
local bx, by = x, y
if state.shiftDown and state.A then
bx, by = snapFromAnchor(state.A, x, y)
end
bx, by = gridSnap(bx, by)
if state.B then
state.B.x, state.B.y = bx, by
end
elseif drag == "TRANSLATE" then
for _, p in ipairs(activePoints()) do
p.x = p.x + rdx
p.y = p.y + rdy
end
elseif drag == "MAYBE_TRANSLATE_OR_ADD" then
if state.dragStart and dist(state.dragStart, {x = x, y = y}) >= 3 then
state.drag = "TRANSLATE"
state.translateStart = {x = x, y = y}
for _, p in ipairs(activePoints()) do
p.x = p.x + rdx
p.y = p.y + rdy
end
end
else
local i = tonumber(drag:match("^P(%d+)$"))
if i and state.points[i] then
state.points[i].x, state.points[i].y = sx, sy
end
end
end)
local function drawPoint(p, col, size)
local half = floor(size / 2)
graphics.fillRect(floor(p.x - half), floor(p.y - half), size, size, col[1], col[2], col[3], col[4])
end
local function previewPoint(p, col, size, zoom)
if zoom then
zDrawPoint(p, col, size)
else
drawPoint(p, col, size)
end
end
local function previewLine(x1, y1, x2, y2, col, zoom)
if zoom then
zDrawLine(x1, y1, x2, y2, col[1], col[2], col[3], col[4])
else
graphics.drawLine(floor(x1), floor(y1), floor(x2), floor(y2), col[1], col[2], col[3], col[4])
end
if not state.mirror then return end
local twinsA = mirrorTwins({x = x1, y = y1})
local twinsB = mirrorTwins({x = x2, y = y2})
if not (twinsA and twinsB) then return end
local dimCol = {floor(col[1] * 0.55), floor(col[2] * 0.55), floor(col[3] * 0.55), col[4]}
for i, ta in ipairs(twinsA) do
local tb = twinsB[i]
if zoom then
zDrawLine(ta.x, ta.y, tb.x, tb.y, dimCol[1], dimCol[2], dimCol[3], dimCol[4])
else
graphics.drawLine(floor(ta.x), floor(ta.y), floor(tb.x), floor(tb.y), dimCol[1], dimCol[2], dimCol[3], dimCol[4])
end
end
end
local function drawHandleLabel(p, a, name)
local ddx, ddy = p.x - a.x, p.y - a.y
local distA = sqrt(ddx * ddx + ddy * ddy)
local angle = deg(atan2(ddy, ddx))
if angle < 0 then angle = angle + 360 end
local txt = string.format("%s %d%s %dpx", name, floor(angle + 0.5), string.char(176), floor(distA + 0.5))
local _, th = graphics.textSize(txt)
graphics.drawText(floor(p.x + 6), floor(p.y - th - 2), txt, COL_DIMYELLOW[1], COL_DIMYELLOW[2], COL_DIMYELLOW[3], COL_DIMYELLOW[4])
end
local function drawCurvePreview(zoom)
local steps = sampleCount(2)
local px, py = curvePoint(0)
for i = 1, steps do
local t = i / steps
local cx, cy = curvePoint(t)
previewLine(px, py, cx, cy, COL_WHITE_PREVIEW, zoom)
px, py = cx, cy
end
end
local function drawLineAdjustOverlay(zoom)
local endpointSize = (state.phase == "ADJUST") and 5 or 3
if state.A then previewPoint(state.A, COL_CYAN, endpointSize, zoom) end
if state.B then previewPoint(state.B, COL_ORANGE, endpointSize, zoom) end
if state.phase == "ADJUST" then
drawCurvePreview(zoom)
if state.mode == "ARC" then
if not zoom then
local mx, my = (state.A.x + state.B.x) / 2, (state.A.y + state.B.y) / 2
local txt = "angle: " .. floor(state.arcAngle + 0.5) .. string.char(176)
graphics.drawText(floor(mx + 6), floor(my - 10), txt, COL_DIMYELLOW[1], COL_DIMYELLOW[2], COL_DIMYELLOW[3], COL_DIMYELLOW[4])
end
else
if state.H1 then
previewPoint(state.H1, COL_YELLOW, 5, zoom)
if not zoom then drawHandleLabel(state.H1, state.A, "H1") end
end
if state.H2 then
previewPoint(state.H2, COL_YELLOW, 5, zoom)
if not zoom then drawHandleLabel(state.H2, state.A, "H2") end
end
end
end
end
local function drawPresetPreview(mouseX, mouseY, hoverActive, zoom)
if not state.origin then return end
local ox, oy = state.origin.x, state.origin.y
local rp = state.radiusPt
if not rp and hoverActive then
local hx, hy = mouseX, mouseY
if state.shiftDown then hx, hy = snapFromAnchor(state.origin, mouseX, mouseY) end
rp = {x = hx, y = hy}
end
if rp then
local savedRP = state.radiusPt
state.radiusPt = rp
local verts = shapeVertices()
state.radiusPt = savedRP
if verts then
for i = 1, #verts do
local a = verts[i]
local b = verts[(i % #verts) + 1]
previewLine(a.x, a.y, b.x, b.y, COL_WHITE_PREVIEW, zoom)
end
end
previewLine(ox, oy, rp.x, rp.y, {COL_DIMYELLOW[1], COL_DIMYELLOW[2], COL_DIMYELLOW[3], 140}, zoom)
previewPoint(rp, COL_ORANGE, 5, zoom)
end
previewPoint(state.origin, COL_CYAN, 5, zoom)
end
local function drawPolyPreview(mouseX, mouseY, hoverActive, zoom)
local pts = state.points
for i = 1, #pts - 1 do
local a, b = pts[i], pts[i + 1]
previewLine(a.x, a.y, b.x, b.y, COL_WHITE_PREVIEW, zoom)
end
if isClosedShape() and #pts >= 3 then
local a, b = pts[#pts], pts[1]
previewLine(a.x, a.y, b.x, b.y, {COL_GREEN[1], COL_GREEN[2], COL_GREEN[3], 180}, zoom)
end
if hoverActive and #pts > 0 and not state.drag then
local last = pts[#pts]
local tx, ty = mouseX, mouseY
if state.shiftDown then tx, ty = snapFromAnchor(last, mouseX, mouseY) end
previewLine(last.x, last.y, tx, ty, {COL_DIMYELLOW[1], COL_DIMYELLOW[2], COL_DIMYELLOW[3], 160}, zoom)
end
for i, p in ipairs(pts) do
local col = (i == 1) and COL_CYAN or ((i == #pts) and COL_ORANGE or COL_YELLOW)
previewPoint(p, col, 5, zoom)
end
end
function dockOrigin(w, h)
local TOP_MARGIN = 34
local SIDE_MARGIN = 2
local BOTTOM_MARGIN = 4
local xr, yr = sim.XRES, sim.YRES
if state.dock == "top-left" then
return SIDE_MARGIN, TOP_MARGIN
elseif state.dock == "top-right" then
return xr - w - SIDE_MARGIN, TOP_MARGIN
elseif state.dock == "top-center" then
return floor((xr - w) / 2), TOP_MARGIN
elseif state.dock == "bottom-left" then
return SIDE_MARGIN, yr - h - BOTTOM_MARGIN
elseif state.dock == "bottom-right" then
return xr - w - SIDE_MARGIN, yr - h - BOTTOM_MARGIN
end
return SIDE_MARGIN, TOP_MARGIN
end
local function modeListLabel()
local modes = CATEGORY_MODES[state.category]
local parts = {}
for i, m in ipairs(modes) do
if m == state.mode then
parts[i] = "[" .. m .. "]"
else
parts[i] = m
end
end
return table.concat(parts, " ")
end
local READY_HINT = "Enter or Right-Click to place"
local function phaseStatus()
if isPreset() then
if state.phase == "EMPTY" then return "click to place center" end
if state.phase == "NEED_RADIUS" then return "click to set radius (angle sets rotation)" end
if state.phase == "ADJUST" then
local r = 0
if state.origin and state.radiusPt then
r = floor(dist(state.origin, state.radiusPt) + 0.5)
end
return "radius: " .. r .. "px | " .. READY_HINT
end
return ""
end
if isPoly() then
local n = #state.points
if n == 0 then return "click to place point 1" end
if n == 1 then return "point 1 set | click to add more (needs 2+ to place)" end
local msg = "points: " .. n .. " | " .. READY_HINT
if isClosedShape() then
msg = msg .. " | will close to P1"
end
return msg
end
if state.phase == "EMPTY" then return "click start point" end
if state.phase == "NEED_B" then return "click end point" end
if state.phase == "ADJUST" then
if state.mode == "ARC" then
return "angle: " .. floor(state.arcAngle + 0.5) .. "deg (+/- 5, ,/. 1) | " .. READY_HINT
end
return "drag handles or endpoints | " .. READY_HINT
end
return ""
end
local function phaseHotkeys()
if isPreset() then
if state.phase == "ADJUST" then
return "drag center/handle [ ]:rot5" .. string.char(176) .. " , .:rot1" .. string.char(176) .. " Shift+drag:snap" .. snapVal() .. string.char(176) .. " Esc:cancel"
end
return "drag center or handle Shift+drag:snap" .. snapVal() .. string.char(176) .. " Esc:cancel"
end
if isPoly() then
return "Click + Drag: Place/Move Bksp:undo Esc:clear"
end
if state.mode == "ARC" then
return "drag A/B +/-:5" .. string.char(176) .. " , .:1" .. string.char(176) .. " Shift+drag:snap" .. snapVal() .. string.char(176) .. " Esc:cancel"
end
return "drag handles Shift+drag:snap" .. snapVal() .. string.char(176) .. " Esc:cancel"
end
local MODE_TLDR = {
QUAD = "curve with 1 shaping handle",
CUBIC = "curve with 2 shaping handles",
ARC = "circle arc between 2 points",
TRI = "triangle from center + radius",
SQUARE = "square from center + radius",
PENT = "pentagon (5 sides)",
HEX = "hexagon (6 sides)",
OCT = "octagon (8 sides)",
STAR = "5-point star",
CIRCLE = "circle (many small segments)",
}
local function modeTldr()
if state.mode == "CUSTOM" then
if state.category == "Shape" then
return "Custom shape that automatically closes"
end
return "many points, straight segments"
end
return MODE_TLDR[state.mode] or ""
end
function drawHud()
local elemName = selectedElemName()
local titleMain = "LINE+ CAT: " .. state.category .. " > [" .. state.mode .. "]"
local titleElem = " elem: " .. elemName
local titleTldr = " -- " .. modeTldr()
local flagParts = {}
if state.grid then flagParts[#flagParts + 1] = "GRID:" .. GRID_SIZE .. "px" end
if state.fill and canFill() then flagParts[#flagParts + 1] = "FILL" end
if state.repeatOn then flagParts[#flagParts + 1] = "REPEAT" end
if state.mirror then flagParts[#flagParts + 1] = "MIRROR:" .. state.mirrorAxis end
local flagsRow = (#flagParts > 0) and ("[" .. table.concat(flagParts, "] [") .. "]") or nil
local modesRow = "modes: " .. modeListLabel()
local modeCount = #CATEGORY_MODES[state.category]
local function hk(action, label)
local kc = state.binds[action]
if kc == nil then return nil end
return keyName(kc) .. ":" .. label
end
local hkParts = {}
local addHk = function(a, l) local s = hk(a, l); if s then hkParts[#hkParts + 1] = s end end
addHk("cycle_category", "cat")
local cycleKc = state.binds.cycle_mode
if cycleKc then
hkParts[#hkParts + 1] = keyName(cycleKc) .. "/1-" .. modeCount .. ":pick"
else
hkParts[#hkParts + 1] = "1-" .. modeCount .. ":pick"
end
addHk("toggle_fill", "fill")
addHk("toggle_repeat", "rep")
addHk("flip_cycle", "flip")
addHk("toggle_mirror", "mirror")
addHk("settings", "settings")
addHk("toggle", "exit")
local globalKeys = table.concat(hkParts, " ")
local phaseKeys = phaseHotkeys()
local status = phaseStatus()
local row1Text = titleMain .. titleElem .. titleTldr
local lines = {
{text = row1Text, col = COL_CYAN},
}
if flagsRow then
lines[#lines + 1] = {text = flagsRow, col = COL_GREEN}
end
lines[#lines + 1] = {text = modesRow, col = COL_DIMGRAY}
lines[#lines + 1] = {text = globalKeys, col = COL_DIMGRAY}
lines[#lines + 1] = {text = phaseKeys, col = COL_DIMGRAY}
lines[#lines + 1] = {text = status, col = COL_YELLOW}
local widest = 0
for _, ln in ipairs(lines) do
if #ln.text > widest then widest = #ln.text end
end
local lineH = 12
local w = widest * 6 + 8
local h = lineH * #lines + 2
local x, y = dockOrigin(w, h)
graphics.fillRect(x, y - 2, w, h, 0, 0, 0, 180)
graphics.drawRect(x, y - 2, w, h, 0, 0, 0, 255)
graphics.drawText(x + 4, y, titleMain,
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
local mainW = graphics.textSize(titleMain)
graphics.drawText(x + 4 + mainW, y, titleElem,
COL_ORANGE[1], COL_ORANGE[2], COL_ORANGE[3], COL_ORANGE[4])
local elemW = graphics.textSize(titleElem)
graphics.drawText(x + 4 + mainW + elemW, y, titleTldr,
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
for i = 2, #lines do
local ln = lines[i]
graphics.drawText(x + 4, y + (i - 1) * lineH, ln.text, ln.col[1], ln.col[2], ln.col[3], ln.col[4])
end
end
local function drawTabBar(x, y, w)
local tabH = 14
local halfW = floor(w / 2)
local genColor = (state.menuTab == "general") and COL_CYAN or COL_DIMGRAY
local kbColor = (state.menuTab == "keybinds") and COL_CYAN or COL_DIMGRAY
graphics.fillRect(x, y, halfW, tabH - 1, 20, 20, 20, 200)
graphics.fillRect(x + halfW, y, w - halfW, tabH - 1, 20, 20, 20, 200)
graphics.drawText(x + 4, y + 3, "General",
genColor[1], genColor[2], genColor[3], genColor[4])
graphics.drawText(x + halfW + 4, y + 3, "Keybinds",
kbColor[1], kbColor[2], kbColor[3], kbColor[4])
state.menuRects[#state.menuRects + 1] = {
x1 = x, y1 = y, x2 = x + halfW, y2 = y + tabH - 1, action = "tab:general",
}
state.menuRects[#state.menuRects + 1] = {
x1 = x + halfW, y1 = y, x2 = x + w, y2 = y + tabH - 1, action = "tab:keybinds",
}
return tabH
end
function drawGeneralTab()
local title = "LINE TOOL+ SETTINGS"
local rows = {
{label = "Category: " .. state.category, action = "category"},
{label = "Mode: " .. state.mode, action = "mode"},
{label = "Dock: " .. state.dock, action = "dock"},
{label = "Snap: " .. snapVal() .. "deg", action = "snap"},
{label = "Grid: " .. (state.grid and "on (8px snap)" or "off"), action = "grid"},
{label = "Fill: " .. (state.fill and "on" or "off"), action = "fill"},
{label = "Repeat: " .. (state.repeatOn and "on" or "off"),action = "repeat"},
{label = "Mirror: " .. (state.mirror and "on" or "off"), action = "mirror"},
{label = "Mirror ax: " .. state.mirrorAxis, action = "mirror_axis"},
{label = "Mobile UI: " .. state.forceMobile ..
(state.forceMobile == "auto" and (state.isMobile and " (on)" or " (off)") or ""),
action = "mobile_ui"},
{label = "[ Reset to defaults ]", action = "reset"},
{label = "[ Close ]", action = "close"},
}
local hint = state.isMobile
and "tap a row to cycle | tap [ Close ] to close"
or ("click a row to cycle | Esc or " .. keyName(state.binds.settings) .. " closes")
local pad = 8
local rowH = 14
local titleH = 14
local tabH = 14
local hintH = 12
local gap = 6
local widest = #title
for _, r in ipairs(rows) do
if #r.label > widest then widest = #r.label end
end
if #hint > widest then widest = #hint end
local w = widest * 6 + pad * 2
local rowsH = rowH * #rows
local h = pad + titleH + gap + tabH + gap + rowsH + gap + hintH + pad
local x = floor((sim.XRES - w) / 2)
local y = floor((sim.YRES - h) / 2)
graphics.fillRect(x, y, w, h, 0, 0, 0, 220)
graphics.drawRect(x, y, w, h, 200, 200, 200, 255)
graphics.drawText(x + pad, y + pad + 2, title,
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
local tabY = y + pad + titleH + gap
drawTabBar(x + pad, tabY, w - pad * 2)
local rowsTop = tabY + tabH + gap
local rowLeft = x + pad
local rowW = w - pad * 2
for i, r in ipairs(rows) do
local ry = rowsTop + (i - 1) * rowH
graphics.fillRect(rowLeft, ry, rowW, rowH - 1, 30, 30, 30, 200)
graphics.drawText(rowLeft + 4, ry + 3, r.label,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
state.menuRects[#state.menuRects + 1] = {
x1 = rowLeft, y1 = ry,
x2 = rowLeft + rowW, y2 = ry + rowH - 1,
action = r.action,
}
end
graphics.drawText(x + pad, y + h - pad - hintH + 2, hint,
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
end
function drawKeybindsTab()
local title = "LINE TOOL+ SETTINGS"
local pad = 8
local rowH = 14
local titleH = 14
local tabH = 14
local hintH = 12
local navH = 14
local gap = 6
local totalPages = ceil(#BIND_ACTIONS / ROWS_PER_PAGE)
if state.bindPage > totalPages then state.bindPage = totalPages end
if state.bindPage < 1 then state.bindPage = 1 end
local startIdx = (state.bindPage - 1) * ROWS_PER_PAGE + 1
local endIdx = min(startIdx + ROWS_PER_PAGE - 1, #BIND_ACTIONS)
local widestLabel = 0
for _, a in ipairs(BIND_ACTIONS) do
if #a.label > widestLabel then widestLabel = #a.label end
end
local labelColW = widestLabel * 6 + 8
local keyColW = 100
local changeBtnW = 50
local clearBtnW = 44
local colGap = 6
local rowW = labelColW + colGap + keyColW + colGap + changeBtnW + colGap + clearBtnW
local hint = state.isMobile
and "tap Change/Clear on a row | switch to General tab, then [ Close ]"
or ("click Change/Clear on a row | Esc or " .. keyName(state.binds.settings) .. " closes")
local navLabel = "Page " .. state.bindPage .. "/" .. totalPages
local headerWidest = max(#title, #hint)
local w = max(headerWidest * 6 + pad * 2, rowW + pad * 2)
local rowsH = rowH * (endIdx - startIdx + 1)
local h = pad + titleH + gap + tabH + gap + rowsH + gap + navH + gap + hintH + pad
local x = floor((sim.XRES - w) / 2)
local y = floor((sim.YRES - h) / 2)
graphics.fillRect(x, y, w, h, 0, 0, 0, 220)
graphics.drawRect(x, y, w, h, 200, 200, 200, 255)
graphics.drawText(x + pad, y + pad + 2, title,
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
local tabY = y + pad + titleH + gap
drawTabBar(x + pad, tabY, w - pad * 2)
local rowsTop = tabY + tabH + gap
local rowLeft = x + pad
for i = startIdx, endIdx do
local a = BIND_ACTIONS[i]
local ry = rowsTop + (i - startIdx) * rowH
graphics.fillRect(rowLeft, ry, rowW, rowH - 1, 30, 30, 30, 200)
graphics.drawText(rowLeft + 4, ry + 3, a.label,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
local keyX = rowLeft + labelColW + colGap
graphics.drawText(keyX + 4, ry + 3, keyName(state.binds[a.name]),
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
local changeX = keyX + keyColW + colGap
graphics.fillRect(changeX, ry, changeBtnW, rowH - 1, 30, 60, 30, 200)
graphics.drawText(changeX + 4, ry + 3, "Change",
COL_GREEN[1], COL_GREEN[2], COL_GREEN[3], COL_GREEN[4])
state.menuRects[#state.menuRects + 1] = {
x1 = changeX, y1 = ry, x2 = changeX + changeBtnW, y2 = ry + rowH - 1,
action = "bind:change:" .. a.name,
}
local clearX = changeX + changeBtnW + colGap
local unclearable = (a.name == "toggle" or a.name == "settings")
if unclearable then
graphics.fillRect(clearX, ry, clearBtnW, rowH - 1, 40, 40, 40, 200)
graphics.drawText(clearX + 4, ry + 3, "locked",
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
else
graphics.fillRect(clearX, ry, clearBtnW, rowH - 1, 60, 30, 30, 200)
graphics.drawText(clearX + 4, ry + 3, "Clear",
COL_ORANGE[1], COL_ORANGE[2], COL_ORANGE[3], COL_ORANGE[4])
state.menuRects[#state.menuRects + 1] = {
x1 = clearX, y1 = ry, x2 = clearX + clearBtnW, y2 = ry + rowH - 1,
action = "bind:clear:" .. a.name,
}
end
end
local navY = rowsTop + rowsH + gap
local prevX1, prevX2 = rowLeft, rowLeft + 50
local nextX1, nextX2 = rowLeft + rowW - 50, rowLeft + rowW
graphics.fillRect(prevX1, navY, prevX2 - prevX1, navH - 1, 30, 30, 30, 200)
graphics.drawText(prevX1 + 4, navY + 3, "< Prev",
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
state.menuRects[#state.menuRects + 1] = {
x1 = prevX1, y1 = navY, x2 = prevX2, y2 = navY + navH - 1, action = "bind:prevpage",
}
graphics.drawText(rowLeft + floor(rowW / 2) - floor(#navLabel * 6 / 2), navY + 3, navLabel,
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
graphics.fillRect(nextX1, navY, nextX2 - nextX1, navH - 1, 30, 30, 30, 200)
graphics.drawText(nextX1 + 4, navY + 3, "Next >",
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
state.menuRects[#state.menuRects + 1] = {
x1 = nextX1, y1 = navY, x2 = nextX2, y2 = navY + navH - 1, action = "bind:nextpage",
}
graphics.drawText(x + pad, y + h - pad - hintH + 2, hint,
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
end
function drawMenu()
state.menuRects = {}
if state.rebindAction then
local a = BIND_LOOKUP[state.rebindAction]
local line1 = "Binding: " .. (a and a.label or state.rebindAction)
local line2 = state.isMobile
and "Press a key (hardware keyboard)"
or "Press any key..."
local line3 = state.isMobile
and "(tap Cancel below)"
or "(Esc to cancel)"
local widest = max(#line1, #line2, #line3, 20)
local w = widest * 6 + 16
local h = 14 * 3 + 16 + (state.isMobile and 20 or 0)
local x = floor((sim.XRES - w) / 2)
local y = floor((sim.YRES - h) / 2)
graphics.fillRect(x, y, w, h, 0, 0, 0, 220)
graphics.drawRect(x, y, w, h, 200, 200, 200, 255)
graphics.drawText(x + 8, y + 8, line1,
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
graphics.drawText(x + 8, y + 22, line2,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
graphics.drawText(x + 8, y + 36, line3,
COL_DIMGRAY[1], COL_DIMGRAY[2], COL_DIMGRAY[3], COL_DIMGRAY[4])
if state.isMobile then
local btnW, btnH = 60, 16
local btnX = x + floor((w - btnW) / 2)
local btnY = y + h - btnH - 4
graphics.fillRect(btnX, btnY, btnW, btnH, 60, 30, 30, 220)
graphics.drawRect(btnX, btnY, btnW, btnH, 200, 200, 200, 255)
local ct = "Cancel"
local ctw = graphics.textSize(ct)
graphics.drawText(btnX + floor((btnW - ctw) / 2), btnY + 4, ct,
COL_ORANGE[1], COL_ORANGE[2], COL_ORANGE[3], COL_ORANGE[4])
state.menuRects[#state.menuRects + 1] = {
x1 = btnX, y1 = btnY, x2 = btnX + btnW, y2 = btnY + btnH,
action = "rebind:cancel",
}
end
return
end
if state.pendingConflict then
local pc = state.pendingConflict
local isModifier = (pc.reason == "modifier")
local line1, line2
if isModifier then
line1 = keyName(pc.key) .. " can't be bound."
line2 = "Modifier keys aren't allowed."
else
local otherLabel = (BIND_LOOKUP[pc.other] and BIND_LOOKUP[pc.other].label) or pc.other
line1 = "Already bound to " .. otherLabel .. "."
line2 = "Replace?"
end
local widest = max(#line1, #line2, 20)
local w = widest * 6 + 16
local h = 14 * 2 + 16 + 18
local x = floor((sim.XRES - w) / 2)
local y = floor((sim.YRES - h) / 2)
graphics.fillRect(x, y, w, h, 0, 0, 0, 220)
graphics.drawRect(x, y, w, h, 200, 200, 200, 255)
graphics.drawText(x + 8, y + 8, line1,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
graphics.drawText(x + 8, y + 22, line2,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
local btnY = y + h - 22
if isModifier then
local okW = 40
local okX1 = x + floor((w - okW) / 2)
local okX2 = okX1 + okW
graphics.fillRect(okX1, btnY, okW, 16, 30, 30, 60, 220)
graphics.drawText(okX1 + 12, btnY + 3, "OK",
COL_CYAN[1], COL_CYAN[2], COL_CYAN[3], COL_CYAN[4])
state.menuRects[#state.menuRects + 1] = {
x1 = okX1, y1 = btnY, x2 = okX2, y2 = btnY + 16, action = "conflict:no",
}
else
local yesX1, yesX2 = x + 8, x + 8 + 40
local noX1, noX2 = x + w - 48, x + w - 8
graphics.fillRect(yesX1, btnY, yesX2 - yesX1, 16, 30, 60, 30, 220)
graphics.drawText(yesX1 + 10, btnY + 3, "Yes",
COL_GREEN[1], COL_GREEN[2], COL_GREEN[3], COL_GREEN[4])
graphics.fillRect(noX1, btnY, noX2 - noX1, 16, 60, 30, 30, 220)
graphics.drawText(noX1 + 12, btnY + 3, "No",
COL_ORANGE[1], COL_ORANGE[2], COL_ORANGE[3], COL_ORANGE[4])
state.menuRects[#state.menuRects + 1] = {
x1 = yesX1, y1 = btnY, x2 = yesX2, y2 = btnY + 16, action = "conflict:yes",
}
state.menuRects[#state.menuRects + 1] = {
x1 = noX1, y1 = btnY, x2 = noX2, y2 = btnY + 16, action = "conflict:no",
}
end
return
end
if state.menuTab == "keybinds" then
drawKeybindsTab()
else
drawGeneralTab()
end
end
function handleMenuClick(mx, my)
for _, r in ipairs(state.menuRects) do
if mx >= r.x1 and mx <= r.x2 and my >= r.y1 and my <= r.y2 then
if r.action == "category" then
cycleCategory()
elseif r.action == "mode" then
cycleMode(1)
elseif r.action == "dock" then
local i = indexOf(DOCK_POSITIONS, state.dock) or 1
state.dock = DOCK_POSITIONS[(i % #DOCK_POSITIONS) + 1]
saveSettings()
elseif r.action == "snap" then
state.snapIdx = (state.snapIdx % #SNAP_VALUES) + 1
saveSettings()
elseif r.action == "grid" then
state.grid = not state.grid
saveSettings()
elseif r.action == "fill" then
state.fill = not state.fill
saveSettings()
elseif r.action == "repeat" then
state.repeatOn = not state.repeatOn
saveSettings()
elseif r.action == "mirror" then
state.mirror = not state.mirror
saveSettings()
elseif r.action == "mirror_axis" then
local i = indexOf(MIRROR_AXES, state.mirrorAxis) or 1
state.mirrorAxis = MIRROR_AXES[(i % #MIRROR_AXES) + 1]
saveSettings()
elseif r.action == "mobile_ui" then
if state.forceMobile == "auto" then state.forceMobile = "on"
elseif state.forceMobile == "on" then state.forceMobile = "off"
else state.forceMobile = "auto" end
recomputeMobile()
saveSettings()
elseif r.action == "reset" then
resetToDefaults()
resetPoints()
elseif r.action == "close" then
state.showMenu = false
elseif r.action == "tab:general" then
state.menuTab = "general"
elseif r.action == "tab:keybinds" then
state.menuTab = "keybinds"
elseif r.action == "bind:prevpage" then
state.bindPage = max(1, state.bindPage - 1)
elseif r.action == "bind:nextpage" then
local totalPages = ceil(#BIND_ACTIONS / ROWS_PER_PAGE)
state.bindPage = min(totalPages, state.bindPage + 1)
elseif r.action == "conflict:yes" then
local pc = state.pendingConflict
if pc then
state.binds[pc.other] = nil
state.binds[pc.action] = pc.key
saveSettings()
end
state.pendingConflict = nil
elseif r.action == "conflict:no" then
state.pendingConflict = nil
elseif r.action == "rebind:cancel" then
state.rebindAction = nil
else
local changeName = r.action:match("^bind:change:(.+)$")
local clearName = r.action:match("^bind:clear:(.+)$")
if changeName and BIND_LOOKUP[changeName] then
state.rebindAction = changeName
elseif clearName and BIND_LOOKUP[clearName] then
if clearName ~= "toggle" and clearName ~= "settings" then
state.binds[clearName] = nil
saveSettings()
end
end
end
return true
end
end
return false
end
local MOBILE_TOOLBAR_BUTTONS = {
{action = "menu", label = "Menu"},
{action = "cycle", label = "Mode"},
{action = "flip", label = "Flip"},
{action = "commit", label = "OK"},
{action = "undo", label = "Undo"},
{action = "cancel", label = "X"},
{action = "off", label = "Off"},
}
local function drawMobileToolbar()
state.mobileRects = {}
local btnW, btnH, gap = 56, 20, 4
local n = #MOBILE_TOOLBAR_BUTTONS
local totalW = n * btnW + (n - 1) * gap
local x0 = floor((sim.XRES - totalW) / 2)
local y0 = 4
for i, b in ipairs(MOBILE_TOOLBAR_BUTTONS) do
local bx = x0 + (i - 1) * (btnW + gap)
graphics.fillRect(bx, y0, btnW, btnH, 30, 30, 30, 220)
graphics.drawRect(bx, y0, btnW, btnH, 200, 200, 200, 255)
local tw = graphics.textSize(b.label)
graphics.drawText(bx + floor((btnW - tw) / 2), y0 + 5, b.label,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
state.mobileRects[#state.mobileRects + 1] = {
x1 = bx, y1 = y0, x2 = bx + btnW, y2 = y0 + btnH, action = b.action,
}
end
end
local function drawMobileActivate()
state.mobileRects = {}
local btnW, btnH = 15, 15
local x0 = sim.XRES + 1
local y0 = floor(sim.YRES / 2) - floor(btnH / 2)
graphics.fillRect(x0, y0, btnW, btnH, 30, 30, 30, 220)
graphics.drawRect(x0, y0, btnW, btnH, 200, 200, 200, 255)
local label = "L+"
local tw = graphics.textSize(label)
graphics.drawText(x0 + floor((btnW - tw) / 2), y0 + 4, label,
COL_YELLOW[1], COL_YELLOW[2], COL_YELLOW[3], COL_YELLOW[4])
state.mobileRects[#state.mobileRects + 1] = {
x1 = x0, y1 = y0, x2 = x0 + btnW, y2 = y0 + btnH, action = "activate",
}
end
local function drawOverlays(zoom)
drawMirrorAxis(zoom)
if isPreset() then
drawPresetPreview(cursor.x, cursor.y, cursor.inSim, zoom)
elseif isPoly() then
drawPolyPreview(cursor.x, cursor.y, cursor.inSim, zoom)
else
drawLineAdjustOverlay(zoom)
end
end
event.register(evt.tick, function()
state.currentTick = state.currentTick + 1
if not state.active then
if state.isMobile and not state.showMenu then
drawMobileActivate()
end
return
end
drawHud()
drawOverlays(false)
if zoomInfo() then
drawOverlays(true)
end
if state.isMobile and not state.showMenu then
drawMobileToolbar()
end
if state.showMenu then drawMenu() end
end)
if state.isMobile then
print("Line Tool+ v2.0 loaded -- tap the L+ button (right sidebar) to activate")
else
print("Line Tool+ v2.0 loaded -- press " .. keyName(state.binds.toggle) .. " to activate")
end
end