├── .gitattributes ├── .gitignore ├── LICENSE ├── Paintshop.lua ├── README.md ├── brushes ├── 01 Sharp.png ├── 02 Middle.png ├── 03 Smooth.png ├── 04 Rough.png ├── 05 Uneven.png ├── 06 Chalk.png ├── 07 Splatter.png ├── 08 Dots.png ├── 09 Leaves.png └── 10 Triangle.png ├── decals ├── Basic Shapes │ ├── Arrow.png │ ├── Circle.png │ ├── Ennegon.png │ ├── Hexagon.png │ ├── Line Narrow.png │ ├── Line.png │ ├── Octagon.png │ ├── Pentagon.png │ ├── Rectangle.png │ ├── Rhombus.png │ ├── Semicircle.png │ ├── Square Hollow.png │ ├── Square Round.png │ ├── Square Semiround.png │ ├── Square Slightly Round.png │ ├── Square.png │ ├── Star 5 Round.png │ ├── Star 5.png │ ├── Star 6 Round.png │ ├── Star 6.png │ ├── Star 8 Round.png │ ├── Star 8.png │ ├── Trapezium.png │ ├── Triangle Round.png │ └── Triangle.png ├── Complex Shapes │ ├── Dolphin.png │ ├── Eagle.png │ ├── Flag.png │ ├── Flames 1.png │ ├── Flames 2.png │ ├── Flames 3.png │ ├── Lines 1.png │ ├── Lines 2.png │ ├── Mess 1.png │ ├── Puzzle 1.png │ ├── Puzzle 2.png │ ├── Shape 1.png │ ├── Shape 2.png │ ├── Shape 3.png │ ├── Shape 4.png │ ├── Shape 5.png │ └── Shape 6.png └── Racing │ ├── E 2.png │ ├── Electric.png │ ├── Esso.png │ ├── Orange.png │ ├── Tow 1.png │ ├── Tow 2.png │ ├── Warning 1.png │ └── Warning 2.png ├── fonts └── ReadMe.txt ├── icon.png ├── manifest.ini └── res └── icons.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dev 2 | .vscode 3 | decals/Logo Stickers/* 4 | decals/Logos/* 5 | decals/Random/* 6 | decals/Tracks/* 7 | fonts/1979.ttf 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Paintshop.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Whole code is a bit of a mess and definitely needs reworking: splitting in separate modules, 3 | reorganazing, etc. But at this stage it’s mostly just a small API test. 4 | ]] 5 | 6 | local sim = ac.getSim() 7 | local uiState = ac.getUI() 8 | local car = ac.getCar(0) 9 | local carDir = ac.getFolder(ac.FolderID.ContentCars)..'/'..ac.getCarID(car.index) 10 | local skinDir = carDir..'/skins/'..ac.getCarSkinID(0) 11 | local carNode = ac.findNodes('carRoot:0') 12 | local carMeshes = carNode:findMeshes('{ ! material:DAMAGE_GLASS & lod:A }') 13 | 14 | -- Calling it once at the start to initialize RealTimeStylus API and get Assetto Corsa to work 15 | -- nicely with pens and styluses (check `ac.getPenPressure()` description for more information). 16 | ac.getPenPressure() 17 | 18 | local selectedMeshes ---@type ac.SceneReference 19 | local carTexture 20 | local aoTexture 21 | 22 | local shortcuts = { 23 | undo = ui.shortcut({ key = ui.KeyIndex.Z, ctrl = true }, ui.KeyIndex.XButton1), 24 | redo = ui.shortcut({ key = ui.KeyIndex.Y, ctrl = true }, ui.KeyIndex.XButton2), 25 | save = ui.shortcut{ key = ui.KeyIndex.S, ctrl = true }, 26 | export = ui.shortcut{ key = ui.KeyIndex.S, ctrl = true, shift = true, alt = true }, 27 | load = ui.shortcut{ key = ui.KeyIndex.O, ctrl = true }, 28 | swapColors = ui.shortcut(ui.KeyIndex.X), 29 | flipSticker = ui.shortcut(ui.KeyIndex.Z), 30 | toggleSymmetry = ui.shortcut(ui.KeyIndex.Y), 31 | toggleDrawThrough = ui.shortcut(ui.KeyIndex.R), 32 | toolBrush = ui.shortcut(ui.KeyIndex.B), 33 | toolEraser = ui.shortcut(ui.KeyIndex.E), 34 | toolStamp = ui.shortcut(ui.KeyIndex.S), 35 | toolMirroringStamp = ui.shortcut(ui.KeyIndex.K), 36 | toolBlurTool = ui.shortcut({ key = ui.KeyIndex.B, alt = true }), 37 | toolEyeDropper = ui.shortcut(ui.KeyIndex.I), 38 | toolMasking = ui.shortcut(ui.KeyIndex.M), 39 | toolText = ui.shortcut(ui.KeyIndex.T), 40 | toggleMasking = ui.shortcut({ key = ui.KeyIndex.M, ctrl = true }), 41 | toggleOrbitCamera = ui.shortcut({ key = ui.KeyIndex.Space, ctrl = true }), 42 | toggleProjectOtherSide = ui.shortcut({ key = ui.KeyIndex.E, ctrl = true }), 43 | arrowLeft = ui.shortcut(ui.KeyIndex.Left), 44 | arrowRight = ui.shortcut(ui.KeyIndex.Right), 45 | arrowUp = ui.shortcut(ui.KeyIndex.Up), 46 | arrowDown = ui.shortcut(ui.KeyIndex.Down), 47 | opacity = table.range(9, 0, function (index) 48 | return ui.shortcut(ui.KeyIndex.D0 + index), index 49 | end) 50 | } 51 | 52 | local icons = ui.atlasIcons('res/icons.png', 4, 4, { 53 | Brush = {1, 1}, 54 | Eraser = {1, 2}, 55 | Undo = {1, 3}, 56 | Redo = {1, 4}, 57 | EyeDropper = {2, 1}, 58 | Camera = {2, 2}, 59 | Save = {2, 3}, 60 | Open = {2, 4}, 61 | Stamp = {3, 1}, 62 | Masking = {3, 2}, 63 | Stencil = {3, 3}, 64 | Export = {3, 4}, 65 | Text = {4, 1}, 66 | MirroringStamp = {4, 2}, 67 | BlurTool = {4, 3}, 68 | MirroringHelper = {4, 4}, 69 | }) 70 | 71 | local taaFix = { On = 1, Off = 0 } 72 | 73 | ac.onRelease(function () 74 | if carTexture and selectedMeshes then 75 | selectedMeshes:setMaterialTexture('txDiffuse', carTexture):setMotionStencil(taaFix.Off) 76 | end 77 | end) 78 | 79 | local carPreview ---@type ac.GeometryShot 80 | local hoveredMaterial 81 | local camera ---@type ac.GrabbedCamera 82 | local appVisible = false 83 | 84 | local function MeshSelection() 85 | local ray = render.createMouseRay() 86 | local ref = ac.emptySceneReference() 87 | if sim.isWindowForeground and carMeshes:raycast(ray, ref) ~= -1 then 88 | ui.text('Found:') 89 | ui.pushFont(ui.Font.Small) 90 | ui.text('\tMesh: '..tostring(ref:name())) 91 | ui.text('\tMaterial: '..tostring(ref:materialName())) 92 | ui.text('\tTexture: '..tostring(ref:getTextureSlotFilename('txDiffuse'))) 93 | ui.popFont() 94 | ui.offsetCursorY(20) 95 | 96 | if hoveredMaterial ~= ref:materialName() then 97 | hoveredMaterial = ref:materialName() 98 | if carPreview then carPreview:dispose() end 99 | carPreview = ac.GeometryShot(carNode:findMeshes('{ material:'..hoveredMaterial..' & lod:A }'), vec2(420, 320)) 100 | carPreview:setClearColor(rgbm(0.14, 0.14, 0.14, 1)) 101 | end 102 | 103 | local mat = mat4x4.rotation(ui.time()*0.1, vec3(0, 1, 0)):mul(car.bodyTransform) 104 | carPreview:update(mat:transformPoint(car.aabbCenter + vec3(0, 2, 4)), mat:transformVector(vec3(0, -1, -2)), nil, 50) 105 | ui.image(carPreview, vec2(210, 160)) 106 | ui.offsetCursorY(20) 107 | 108 | local size = ui.imageSize(ref:getTextureSlotFilename('txDiffuse')) 109 | if size.x > 0 and size.y > 0 then 110 | 111 | ui.textWrapped('• Hold Shift and click to start drawing.\n• Hold Ctrl+Shift and click to start drawing using custom AO map.') 112 | 113 | ui.offsetCursorY(20) 114 | ui.pushFont(ui.Font.Small) 115 | ui.textWrapped('For best results, either use a custom AO map or make sure this texture is an AO map (grayscale colors with nothing but shadows).') 116 | ui.popFont() 117 | 118 | ui.setShadingOffset(1, 0, 1, 1) 119 | ui.image(ref:getTextureSlotFilename('txDiffuse'), vec2(210, 210 * size.y / size.x)) 120 | ui.resetShadingOffset() 121 | 122 | if uiState.shiftDown and not uiState.altDown and uiState.isMouseLeftKeyClicked and not uiState.wantCaptureMouse then 123 | if uiState.ctrlDown then 124 | local _selectedMeshes = carNode:findMeshes('{ material:'..hoveredMaterial..' & lod:A }') 125 | local _carTexture = ref:getTextureSlotFilename('txDiffuse') 126 | os.openFileDialog({ 127 | title = 'Open Base AO Map', 128 | folder = carDir, 129 | fileTypes = { { name = 'Images', mask = '*.png;*.jpg;*.jpeg;*.dds' } }, 130 | addAllFilesFileType = true, 131 | flags = bit.bor(os.DialogFlags.PathMustExist, os.DialogFlags.FileMustExist) 132 | }, function (err, filename) 133 | if not err and filename then 134 | selectedMeshes = _selectedMeshes 135 | carTexture = _carTexture 136 | aoTexture = filename 137 | camera = ac.grabCamera('Paintshop') 138 | if camera then camera.ownShare = 0 end 139 | end 140 | end) 141 | else 142 | selectedMeshes = carNode:findMeshes('{ material:'..hoveredMaterial..' & lod:A }') 143 | carTexture = ref:getTextureSlotFilename('txDiffuse') 144 | aoTexture = nil 145 | camera = ac.grabCamera('Paintshop') 146 | if camera then camera.ownShare = 0 end 147 | end 148 | end 149 | else 150 | ui.text('Texture is missing') 151 | end 152 | else 153 | ui.text('Hover a car mesh to start drawing…') 154 | end 155 | end 156 | 157 | local editingCanvas, aoCanvas, maskingCanvas ---@type ui.ExtraCanvas 158 | local editingCanvasPhase = 0 159 | local lastRay ---@type ray 160 | 161 | local stored = ac.storage{ 162 | color = rgbm(0, 0.2, 1, 0.5), 163 | bgColor = rgbm(1, 1, 1, 1), 164 | orbitCamera = true, 165 | projectOtherSide = false, 166 | eyeDropperRange = 1, 167 | selectedStickerSet = 2, 168 | alignSticker = 3, 169 | activeToolIndex = 1, 170 | selectedFont = '', 171 | fontBold = false, 172 | fontItalic = false, 173 | hasPen = false 174 | } 175 | 176 | local function brushSizeMult(brush) 177 | local p = ac.getPenPressure() 178 | if p ~= 1 and not stored.hasPen then stored.hasPen = true end 179 | return math.lerp(brush.penMinRadiusMult, 1, p) 180 | end 181 | 182 | local function brushParams(key, defaultSize, defaultAlpha, extraFields) 183 | local t = { 184 | brushTex = '', 185 | brushSize = defaultSize or 0.05, 186 | brushAspectMult = 1, 187 | brushStepSize = 0.005, 188 | brushAngle = 0, 189 | brushRandomizedAngle = false, 190 | brushAlpha = defaultAlpha or 0.5, 191 | brushMirror = false, 192 | penMinRadiusMult = 0.05, 193 | withMirror = false, 194 | paintThrough = false, 195 | smoothing = 0 196 | } 197 | if extraFields then 198 | for k, v in pairs(extraFields) do t[k] = v end 199 | end 200 | return ac.storage(t, key) 201 | end 202 | 203 | local ignoreMousePress = true 204 | local drawing = false 205 | local brushesDir = __dirname..'/brushes' 206 | local decalsDir = __dirname..'/decals' 207 | local brushes 208 | local stickers 209 | local selectedStickerSet 210 | local selectedBrushOutline ---@type ui.ExtraCanvas 211 | local selectedBrushOutlineDirty = true 212 | local brushDistance = 1 213 | local cameraAngle = vec2(-2.6, 0.1) 214 | local maskingDragging = 0 215 | local changesMade = 0 216 | local saveFilename 217 | -- local maskingCarStored = {} ---@type ac.GeometryShot 218 | local undoStack = {} 219 | local redoStack = {} 220 | 221 | local maskingActive = false 222 | local maskingPos = vec3(0, 0.3, 0) 223 | local maskingDir = vec3(0, 1, 0) 224 | local maskingCreatingFrom, maskingCreatingTo 225 | local maskingPoints = { 226 | vec3(0, 0.3, -1), 227 | vec3(0, 0.3, 1), 228 | vec3(-1, 0.3, 0), 229 | vec3(1, 0.3, 0), 230 | } 231 | 232 | local function drawWithAO(baseCanvas, aoTexture) 233 | -- Draw base editing canvas and apply AO to it. One way of doing it is to use shading offset: 234 | -- ui.drawImage(aoTexture, 0, ui.windowSize()) 235 | -- ui.setShadingOffset(0, 0, 0, -1) 236 | -- ui.drawImage(aoTexture, 0, ui.windowSize(), rgbm.colors.black) 237 | -- ui.resetShadingOffset() 238 | 239 | -- But now there is another way, to use a custom shader: 240 | ui.renderShader({ 241 | p1 = vec2(), 242 | p2 = ui.windowSize(), 243 | blendMode = render.BlendMode.Opaque, 244 | textures = { 245 | txBase = baseCanvas, 246 | txAO = aoTexture 247 | }, 248 | shader = [[float4 main(PS_IN pin) { 249 | float4 diffuseColor = txAO.SampleLevel(samLinear, pin.Tex, 0); 250 | float4 canvasColor = txBase.SampleLevel(samLinear, pin.Tex, 0); 251 | canvasColor.rgb *= max(diffuseColor.r, max(diffuseColor.g, diffuseColor.b)); // use maximum value of AO RGB color 252 | canvasColor.a = 1; // return fully opaque texture so that txDetail would not bleed and CMAA2 would be happy 253 | return canvasColor; 254 | }]] 255 | }) 256 | end 257 | 258 | local function finishEditing() 259 | selectedMeshes:setMaterialTexture('txDiffuse', carTexture):setMotionStencil(taaFix.Off) 260 | selectedMeshes = nil 261 | carTexture = nil 262 | editingCanvas = nil 263 | saveFilename = nil 264 | -- maskingCarView = nil 265 | undoStack = {} 266 | redoStack = {} 267 | changesMade = 0 268 | ac.setWindowTitle('paintshop', nil) 269 | 270 | if camera then 271 | local cameraRelease 272 | cameraRelease = setInterval(function () 273 | camera.ownShare = math.applyLag(camera.ownShare, 0, 0.85, ac.getDeltaT()) 274 | if camera.ownShare < 0.001 then 275 | clearInterval(cameraRelease) 276 | camera:dispose() 277 | camera = nil 278 | end 279 | end) 280 | end 281 | end 282 | 283 | local function rescanBrushes() 284 | brushes = table.map(io.scanDir(brushesDir, '*.png'), function (x) return { string.sub(x, 1, #x - 4), brushesDir..'/'..x } end) 285 | end 286 | 287 | local function rescanStickers() 288 | stickers = table.map(io.scanDir(decalsDir, '*'), function (x) return { 289 | name = x, 290 | items = table.map(io.scanDir(decalsDir..'/'..x, '*.png'), function (y) return { string.sub(y, 1, #y - 4), decalsDir..'/'..x..'/'..y } end) 291 | } end) 292 | selectedStickerSet = stickers[stored.selectedStickerSet] 293 | end 294 | 295 | local accessibleData ---@type ui.ExtraCanvasData 296 | 297 | local function maskingBackup() 298 | local b = stringify({ maskingPos, maskingDir, maskingPoints }, true) 299 | return function (action) 300 | if action == 'memoryFootprint' then return 0 end 301 | if action == 'update' then return maskingBackup() end 302 | if action == 'dispose' then return end 303 | maskingPos, maskingDir, maskingPoints = table.unpack(stringify.parse(b)) 304 | maskingActive = true 305 | end 306 | end 307 | 308 | local function addUndo(undo) 309 | if #undoStack > 29 then 310 | undoStack[1]('dispose') 311 | table.remove(undoStack, 1) 312 | end 313 | table.insert(undoStack, undo) 314 | table.clear(redoStack) 315 | changesMade = changesMade + 1 316 | end 317 | 318 | local function stepUndo() 319 | local last = undoStack[#undoStack] 320 | if not last then return end 321 | table.insert(redoStack, last('update')) 322 | last() 323 | last('dispose') 324 | table.remove(undoStack) 325 | changesMade = changesMade - 1 326 | editingCanvasPhase = editingCanvasPhase + 1 327 | end 328 | 329 | local function stepRedo() 330 | local last = redoStack[#redoStack] 331 | if not last then return end 332 | table.insert(undoStack, last('update')) 333 | last() 334 | last('dispose') 335 | table.remove(redoStack) 336 | changesMade = changesMade + 1 337 | editingCanvasPhase = editingCanvasPhase + 1 338 | end 339 | 340 | local function undoMemoryFootpring() 341 | return table.sum(undoStack, function (u) return u('memoryFootprint') end) 342 | + table.sum(redoStack, function (u) return u('memoryFootprint') end) 343 | end 344 | 345 | local function updateAccessibleData() 346 | editingCanvasPhase = editingCanvasPhase + 1 347 | if accessibleData then accessibleData:dispose() end 348 | editingCanvas:accessData(function (err, data) 349 | if data then accessibleData = data 350 | elseif err then ac.warn('Failed to access canvas: '..tostring(err)) end 351 | end) 352 | end 353 | 354 | local autosaveDir = ac.getFolder(ac.FolderID.Cfg)..'/apps/paintshop/autosave' 355 | local autosaveIndex = 1 356 | local autosavePhase = 0 357 | 358 | setInterval(function () 359 | if not editingCanvas or autosavePhase == editingCanvasPhase or uiState.isMouseLeftKeyDown then return end 360 | autosavePhase = editingCanvasPhase 361 | io.createDir(autosaveDir) 362 | editingCanvas:save(string.format('%s/autosave-%s.zip', autosaveDir, autosaveIndex), ac.ImageFormat.ZippedDDS) 363 | autosaveIndex = autosaveIndex + 1 364 | if autosaveIndex == 10 then autosaveIndex = 1 end 365 | end, 20) 366 | 367 | local function IconButton(icon, tooltip, active, enabled) 368 | local r = ui.button('##'..icon, vec2(32, 32), enabled == false and ui.ButtonFlags.Disabled or active and ui.ButtonFlags.Active or ui.ButtonFlags.None) 369 | ui.addIcon(icon, 24, 0.5, nil, 0) 370 | if tooltip and ui.itemHovered() then ui.setTooltip(tooltip) end 371 | return r 372 | end 373 | 374 | local function DrawControl() 375 | ac.setWindowTitle('paintshop', string.gsub(saveFilename and saveFilename or carTexture..' (new)', '.+[/\\:]', '')..(changesMade ~= 0 and '*' or '')) 376 | 377 | if IconButton(icons.Undo, nil, false, #undoStack > 0) or #undoStack > 0 and shortcuts.undo() then 378 | stepUndo() 379 | end 380 | if ui.itemHovered() then 381 | ui.setTooltip(string.format('Undo (Ctrl+Z)', #undoStack, math.ceil(undoMemoryFootpring() / (1024 * 1024)))) 382 | end 383 | ui.sameLine(0, 4) 384 | if IconButton(icons.Redo, string.format('Redo (Ctrl+Y)', #redoStack), false, #redoStack > 0) or #redoStack > 0 and shortcuts.redo() then 385 | stepRedo() 386 | end 387 | ui.sameLine(0, 4) 388 | if IconButton(icons.Open, 'Load image (Ctrl+O)\n\nChoose an image without ambient occlusion, preferably one saved earlier with “Save” button of this tool.\n\nIf you accidentally forgot to save or a crash happened, there are some automatically saved backups\nin “Documents/Assetto Corsa”/cfg/apps/paintshop/autosave”.\n\n(There is also an “Import” option in context menu of this button to add a semi-transparent image on top\nof current one.)') or shortcuts.load() then 389 | os.openFileDialog({ 390 | title = 'Open', 391 | folder = skinDir, 392 | fileTypes = { { name = 'Images', mask = '*.png;*.jpg;*.jpeg;*.dds' } }, 393 | }, function (err, filename) 394 | if not err and filename then 395 | ui.setAsynchronousImagesLoading(false) 396 | addUndo(editingCanvas:backup()) 397 | editingCanvas:clear(rgbm.new(stored.bgColor.rgb, 1)):update(function () 398 | ui.unloadImage(filename) 399 | ui.drawImage(filename, 0, ui.windowSize()) 400 | end) 401 | setTimeout(updateAccessibleData) 402 | if not filename:lower():match('%.dds$') then 403 | saveFilename = filename 404 | end 405 | changesMade = 0 406 | end 407 | end) 408 | end 409 | ui.itemPopup('openMenu', function () 410 | if ui.selectable('Clear canvas') then 411 | addUndo(editingCanvas:backup()) 412 | editingCanvas:clear(rgbm.new(stored.bgColor.rgb, 1)) 413 | end 414 | if ui.itemHovered() then 415 | ui.setTooltip('Clears canvas using background (eraser) color') 416 | end 417 | if ui.selectable('Import…') then 418 | os.openFileDialog({ 419 | title = 'Import', 420 | folder = skinDir, 421 | fileTypes = { { name = 'Images', mask = '*.png;*.jpg;*.jpeg;*.dds' } }, 422 | }, function (err, filename) 423 | if not err and filename then 424 | ui.setAsynchronousImagesLoading(false) 425 | addUndo(editingCanvas:backup()) 426 | editingCanvas:update(function () 427 | ui.unloadImage(filename) 428 | ui.drawImage(filename, 0, ui.windowSize()) 429 | end) 430 | setTimeout(updateAccessibleData) 431 | end 432 | end) 433 | end 434 | if autosaveDir and ui.selectable('Open autosaves folder') then 435 | io.createDir(autosaveDir) 436 | os.openInExplorer(autosaveDir) 437 | end 438 | end) 439 | ui.sameLine(0, 4) 440 | if IconButton(icons.Save, 'Save image (Ctrl+S)\n\nImage saved like that would not have antialiasing or ambient occlusion. To apply texture, use “Export texture”\nbutton on the right.\n\n(There is also a “Save as” option in context menu of this button.)') or shortcuts.save() then 441 | if saveFilename ~= nil then 442 | editingCanvas:save(saveFilename) 443 | changesMade = 0 444 | else 445 | os.saveFileDialog({ 446 | title = 'Save Image', 447 | folder = skinDir, 448 | fileTypes = { { name = 'PNG', mask = '*.png' }, { name = 'JPEG', mask = '*.jpg;*.jpeg' } }, 449 | fileName = carTexture and string.gsub(carTexture, '.+[/\\:]', ''):gsub('%.[a-zA-Z]+$', '.png'), 450 | defaultExtension = 'dds', 451 | }, function (err, filename) 452 | if not err and filename then 453 | editingCanvas:save(filename) 454 | saveFilename = filename 455 | changesMade = 0 456 | end 457 | end) 458 | end 459 | end 460 | ui.itemPopup('saveMenu', function () 461 | if ui.selectable('Save as…') then 462 | os.saveFileDialog({ 463 | title = 'Save Image As', 464 | folder = skinDir, 465 | fileTypes = { { name = 'PNG', mask = '*.png' }, { name = 'JPEG', mask = '*.jpg;*.jpeg' } }, 466 | fileName = carTexture and string.gsub(carTexture, '.+[/\\:]', ''):gsub('%.[a-zA-Z]+$', '.png'), 467 | defaultExtension = 'dds', 468 | }, function (err, filename) 469 | if not err and filename then 470 | editingCanvas:save(filename) 471 | saveFilename = filename 472 | changesMade = 0 473 | end 474 | end) 475 | end 476 | if autosaveDir and ui.selectable('Open autosaves folder') then 477 | io.createDir(autosaveDir) 478 | os.openInExplorer(autosaveDir) 479 | end 480 | end) 481 | ui.sameLine(0, 4) 482 | if IconButton(icons.Export, 'Export texture (Ctrl+Shift+Alt+S)\n\nImage saved like that is ready to use, with ambient occlusion and everything. To save an intermediate\nresult and continue working on it later, use “Save” button on the left.') or shortcuts.export() then 483 | os.saveFileDialog({ 484 | title = 'Export Texture', 485 | folder = skinDir, 486 | fileTypes = { { name = 'PNG', mask = '*.png' }, { name = 'JPEG', mask = '*.jpg;*.jpeg' }, { name = 'DDS', mask = '*.dds' } }, 487 | fileName = carTexture and string.gsub(carTexture, '.+[/\\:]', ''), 488 | fileTypeIndex = 3, 489 | defaultExtension = 'dds', 490 | }, function (err, filename) 491 | if not err and filename then 492 | aoCanvas:update(function (dt) 493 | drawWithAO(editingCanvas, aoTexture or carTexture) 494 | end):save(filename) 495 | end 496 | end) 497 | end 498 | ui.sameLine(0, 4) 499 | if IconButton(ui.Icons.Leave, changesMade == 0 and 'Finish editing' or 'Cancel editing\nThere are some unsaved changes') then 500 | if changesMade ~= 0 then 501 | ui.modalPopup('Cancel editing', 'Are you sure to exit without saving changes?', function (okPressed) 502 | if okPressed then 503 | finishEditing() 504 | end 505 | end) 506 | else 507 | finishEditing() 508 | end 509 | end 510 | end 511 | 512 | local palette = { 513 | builtin = { 514 | rgbm(1, 1, 1, 1), 515 | rgbm(0.8, 0.8, 0.8, 1), 516 | rgbm(0.6, 0.6, 0.6, 1), 517 | rgbm(1, 0, 0, 1), 518 | rgbm(1, 0.5, 0, 1), 519 | rgbm(1, 1, 0, 1), 520 | rgbm(0.5, 1, 0, 1), 521 | rgbm(0, 1, 0, 1), 522 | rgbm(0, 1, 0.5, 1), 523 | rgbm(0, 1, 1, 1), 524 | rgbm(0, 0.5, 1, 1), 525 | rgbm(0, 0, 1, 1), 526 | rgbm(0.5, 0, 1, 1), 527 | rgbm(1, 0, 1, 1), 528 | rgbm(1, 0, 0.5, 1), 529 | rgbm(0, 0, 0, 1), 530 | rgbm(0.2, 0.2, 0.2, 1), 531 | rgbm(0.4, 0.4, 0.4, 1), 532 | rgbm(1, 0, 0, 1):scale(0.5), 533 | rgbm(1, 0.5, 0, 1):scale(0.5), 534 | rgbm(1, 1, 0, 1):scale(0.5), 535 | rgbm(0.5, 1, 0, 1):scale(0.5), 536 | rgbm(0, 1, 0, 1):scale(0.5), 537 | rgbm(0, 1, 0.5, 1):scale(0.5), 538 | rgbm(0, 1, 1, 1):scale(0.5), 539 | rgbm(0, 0.5, 1, 1):scale(0.5), 540 | rgbm(0, 0, 1, 1):scale(0.5), 541 | rgbm(0.5, 0, 1, 1):scale(0.5), 542 | rgbm(1, 0, 1, 1):scale(0.5), 543 | rgbm(1, 0, 0.5, 1):scale(0.5), 544 | }, 545 | user = stringify.tryParse(ac.storage.palette) or table.range(15, function (index, callbackData) 546 | return rgbm(math.random(), math.random(), math.random(), 1) 547 | end) 548 | } 549 | 550 | function palette.addToUserPalette(color) 551 | local _, i = table.findFirst(palette.user, function (item) return item == color end) 552 | if i ~= nil then 553 | table.remove(palette.user, i) 554 | else 555 | table.remove(palette.user, 1) 556 | end 557 | table.insert(palette.user, color:clone()) 558 | ac.storage.palette = stringify(palette.user, true) 559 | end 560 | 561 | local function ColorTooltip(color) 562 | ui.tooltip(0, function () 563 | ui.dummy(20) 564 | ui.drawRectFilled(0, 20, color) 565 | ui.drawRect(0, 20, rgbm.colors.black) 566 | end) 567 | end 568 | 569 | local editing = false 570 | local colorFlags = bit.bor(ui.ColorPickerFlags.NoAlpha, ui.ColorPickerFlags.NoSidePreview, ui.ColorPickerFlags.PickerHueWheel, ui.ColorPickerFlags.DisplayHex) 571 | 572 | local function ColorBlock(key) 573 | key = key or 'color' 574 | local col = stored[key]:clone() 575 | ui.colorPicker('##color', col, colorFlags) 576 | if ui.itemEdited() then 577 | stored[key] = col 578 | editing = true 579 | elseif editing and not ui.itemActive() then 580 | editing = false 581 | palette.addToUserPalette(col) 582 | end 583 | for i = 1, #palette.builtin do 584 | ui.drawRectFilled(ui.getCursor(), ui.getCursor() + 14, palette.builtin[i]) 585 | if ui.invisibleButton(i, 14) then 586 | stored[key] = palette.builtin[i]:clone() 587 | palette.addToUserPalette(stored[key]) 588 | end 589 | if ui.itemHovered() then 590 | ColorTooltip(palette.builtin[i]) 591 | end 592 | ui.sameLine(0, 0) 593 | if ui.availableSpaceX() < 14 then 594 | ui.newLine(0) 595 | end 596 | end 597 | for i = 1, #palette.user do 598 | ui.drawRectFilled(ui.getCursor(), ui.getCursor() + 14, palette.user[i]) 599 | if ui.invisibleButton(100 + i, 14) then 600 | stored[key] = palette.user[i]:clone() 601 | palette.addToUserPalette(stored[key]) 602 | end 603 | if ui.itemHovered() then 604 | ColorTooltip(palette.user[i]) 605 | end 606 | ui.sameLine(0, 0) 607 | end 608 | ui.newLine() 609 | if shortcuts.swapColors() then 610 | stored[key] = stored[key] == palette.user[#palette.user] and palette.user[#palette.user - 1] or palette.user[#palette.user] 611 | palette.addToUserPalette(stored[key]) 612 | end 613 | end 614 | 615 | local function BrushBaseBlock(brush, maxSize, stickerMode, noStepSize, noSymmetry) 616 | if not ui.mouseBusy() then 617 | local w = ui.mouseWheel() 618 | if ui.keyboardButtonPressed(ui.KeyIndex.SquareOpenBracket, true) then w = w - 1 end 619 | if ui.keyboardButtonPressed(ui.KeyIndex.SquareCloseBracket, true) then w = w + 1 end 620 | if w ~= 0 then -- changing brush size with mouse wheel 621 | if uiState.shiftDown then w = w / 10 end 622 | if uiState.altDown then 623 | brush.brushAngle = brush.brushAngle + w * 30 624 | elseif not uiState.ctrlDown then 625 | brush.brushSize = math.clamp(brush.brushSize * (1 + w * 0.15), 0.001, maxSize) 626 | elseif stickerMode then 627 | brush.brushAspectMult = math.clamp(brush.brushAspectMult * (1 + w * 0.25), 0.04, 25) 628 | end 629 | selectedBrushOutlineDirty = true 630 | end 631 | for i = 0, 9 do -- changing opacity photoshop style 632 | if shortcuts.opacity[i]() then brush.brushAlpha = i == 0 and 1 or i / 10 end 633 | end 634 | end 635 | 636 | if stickerMode then 637 | if ui.checkbox('Flip sticker', brush.brushMirror) or shortcuts.flipSticker() then brush.brushMirror = not brush.brushMirror end 638 | if ui.itemHovered() then ui.setTooltip('Flip sticker (Z)') end 639 | end 640 | 641 | brush.brushSize = ui.slider('##brushSize', brush.brushSize * 100, 0.1, maxSize * 100, 'Size: %.1f cm', 2) / 100 642 | if ui.itemHovered() then ui.setTooltip('Use mouse wheel to quickly change size') end 643 | if ui.itemEdited() then selectedBrushOutlineDirty = true end 644 | 645 | if stored.hasPen then 646 | brush.penMinRadiusMult = ui.slider('##penMinRadiusMult', brush.penMinRadiusMult * 100, 0, 100, 'Minimum size: %.1f%%') / 100 647 | if ui.itemHovered() then ui.setTooltip('Size of a brush with minimum pen pressure') end 648 | end 649 | 650 | if stickerMode then 651 | ui.setNextItemWidth(ui.availableSpaceX() - 60) 652 | brush.brushAspectMult = ui.slider('##brushAspectMult', brush.brushAspectMult * 100, 4, 2500, 'Stretch: %.0f%%', 4) / 100 653 | if ui.itemHovered() then ui.setTooltip('Use mouse wheel and hold Ctrl to quickly change size') end 654 | if ui.itemEdited() then selectedBrushOutlineDirty = true end 655 | ui.sameLine(0, 4) 656 | if ui.button('Reset', vec2(56, 0)) then 657 | brush.brushAspectMult = 1 658 | selectedBrushOutlineDirty = true 659 | end 660 | end 661 | 662 | if not stickerMode and not noStepSize then 663 | brush.brushStepSize = ui.slider('##brushStepSize', brush.brushStepSize * 100, 0.1, 50, 'Step size: %.1f cm', 2) / 100 664 | end 665 | 666 | brush.brushAlpha = ui.slider('##alpha', brush.brushAlpha * 100, 0, 100, 'Opacity: %.1f%%') / 100 667 | if ui.itemHovered() then ui.setTooltip('Use digit buttons to quickly change opacity') end 668 | 669 | if ui.checkbox('##randomAngle', brush.brushRandomizedAngle) then brush.brushRandomizedAngle = not brush.brushRandomizedAngle end 670 | if ui.itemHovered() then ui.setTooltip('Randomize angle when drawing') end 671 | ui.sameLine(0, 4) 672 | ui.setNextItemWidth(210 - 22 - 4 - 60) 673 | brush.brushAngle = (brush.brushAngle % 360 + 360) % 360 674 | brush.brushAngle = ui.slider('##brushAngle', brush.brushAngle, 0, 360, 'Angle: %.0f°') 675 | if ui.itemHovered() then ui.setTooltip('Use mouse wheel and hold Alt to quickly change angle') end 676 | ui.sameLine(0, 4) 677 | if ui.button('Reset##angle', vec2(56, 0)) then 678 | brush.brushAngle = 0 679 | end 680 | 681 | if not stickerMode and not noStepSize then 682 | brush.smoothing = ui.slider('##smoothing', brush.smoothing * 100, 0, 100, 'Smoothing: %.1f%%') / 100 683 | if ui.itemHovered() then ui.setTooltip('Smoothing makes brush move smoother and slower') end 684 | end 685 | 686 | if not noSymmetry then 687 | if ui.checkbox('With symmetry', brush.withMirror) or shortcuts.toggleSymmetry() then brush.withMirror = not brush.withMirror end 688 | if ui.itemHovered() then ui.setTooltip('Paith with symmetry (Y)\nMirrors things from one side of a car to another') end 689 | end 690 | 691 | if ui.checkbox('Paint through', brush.paintThrough) or shortcuts.toggleDrawThrough() then brush.paintThrough = not brush.paintThrough end 692 | if ui.itemHovered() then ui.setTooltip('Paint through model (R)\nIf enabled, drawings would go through model and leave traces on the opposite side as well') end 693 | end 694 | 695 | local function BrushBlock(brush) 696 | if brush.brushTex == '' then brush.brushTex = brushes[1][2] end 697 | local anySelected = false 698 | ui.childWindow('brushesList', vec2(210, 60), false, bit.bor(ui.WindowFlags.HorizontalScrollbar, ui.WindowFlags.AlwaysHorizontalScrollbar, ui.WindowFlags.NoBackground), function () 699 | ui.pushStyleColor(ui.StyleColor.Button, rgbm.colors.transparent) 700 | for i = 1, #brushes do 701 | local selected = brushes[i][2] == brush.brushTex 702 | if ui.button('##'..i, 48, selected and ui.ButtonFlags.Active or ui.ButtonFlags.None) then 703 | brush.brushTex = brushes[i][2] 704 | selectedBrushOutlineDirty = true 705 | end 706 | if selected then 707 | anySelected = true 708 | end 709 | ui.addIcon(brushes[i][2], 36, 0.5, nil, 0) 710 | if ui.itemHovered() then ui.setTooltip('Brush: '..brushes[i][1]) end 711 | ui.sameLine(0, 4) 712 | end 713 | ui.popStyleColor() 714 | ui.newLine() 715 | end) 716 | if not anySelected then 717 | brush.brushTex = brushes[1][2] 718 | end 719 | ui.itemPopup(function () 720 | if ui.selectable('Open in Explorer') then 721 | os.openInExplorer(brushesDir) 722 | end 723 | if ui.selectable('Refresh') then 724 | rescanBrushes() 725 | end 726 | end) 727 | end 728 | 729 | local function fitMaskingPoints(fitFirst) 730 | if fitFirst then 731 | maskingDir = math.cross(maskingPoints[1] - maskingPoints[2], maskingPoints[4] - maskingPoints[3]):normalize() 732 | maskingPos = (maskingPoints[1] + maskingPoints[2]) / 2 733 | local ort1 = math.cross(maskingDir, vec3(1, 0, 0)):normalize() 734 | local ort2 = math.cross(maskingDir, vec3(0, 0, 1)):normalize() 735 | maskingPoints[3] = vec3(maskingPoints[3].x, maskingPos.y - maskingPos.z * ort1.y / ort1.z + ort2.y * maskingPoints[3].x / ort2.x, 0) 736 | maskingPoints[4] = vec3(maskingPoints[4].x, maskingPos.y - maskingPos.z * ort1.y / ort1.z + ort2.y * maskingPoints[4].x / ort2.x, 0) 737 | else 738 | maskingDir = math.cross(maskingPoints[1] - maskingPoints[2], maskingPoints[4] - maskingPoints[3]):normalize() 739 | maskingPos = (maskingPoints[3] + maskingPoints[4]) / 2 740 | local ort2 = math.cross(maskingDir, vec3(0, 0, 1)):normalize() 741 | local ort1 = math.cross(maskingDir, vec3(1, 0, 0)):normalize() 742 | maskingPoints[1] = vec3(0, maskingPos.y - maskingPos.x * ort2.y / ort2.x + ort1.y * maskingPoints[1].z / ort1.z, maskingPoints[1].z) 743 | maskingPoints[2] = vec3(0, maskingPos.y - maskingPos.x * ort2.y / ort2.x + ort1.y * maskingPoints[2].z / ort1.z, maskingPoints[2].z) 744 | end 745 | end 746 | 747 | local function applyQuickMasking(from, to) 748 | if math.abs(from.x - to.x) < math.abs(from.z - to.z) then 749 | maskingPoints[1] = vec3(0, from.y, from.z) 750 | maskingPoints[2] = vec3(0, to.y, to.z) 751 | maskingPoints[3] = vec3(-1, 0, 0) 752 | maskingPoints[4] = vec3(1, 0, 0) 753 | fitMaskingPoints(true) 754 | else 755 | maskingPoints[1] = vec3(0, 0, -1) 756 | maskingPoints[2] = vec3(0, 0, 1) 757 | maskingPoints[3] = vec3(from.x, from.y, 0) 758 | maskingPoints[4] = vec3(to.x, to.y, 0) 759 | fitMaskingPoints(false) 760 | end 761 | end 762 | 763 | local function getBrushUp(dir, tool) 764 | local brush = tool.brush 765 | return mat4x4.rotation(math.rad(brush.brushRandomizedAngle and tool.__brushRandomAngle or brush.brushAngle), dir):transformVector(car.up) 766 | end 767 | 768 | local fonts 769 | local fontsDir = __dirname..'/fonts' 770 | local function rescanFonts() 771 | fonts = { 772 | { name = 'Arial', source = 'Arial:@System' }, 773 | { name = 'Bahnschrift', source = 'Bahnschrift:@System' }, 774 | { name = 'Calibri', source = 'Calibri:@System' }, 775 | { name = 'Comic Sans MS', source = 'Comic Sans MS:@System' }, 776 | { name = 'Consolas', source = 'Consolas' }, 777 | { name = 'Courier New', source = 'Courier New:@System' }, 778 | { name = 'Impact', source = 'Impact:@System' }, 779 | { name = 'Orbitron', source = 'Orbitron' }, 780 | { name = 'Segoe UI', source = 'Segoe UI' }, 781 | { name = 'Times New Roman', source = 'Times New Roman:@System' }, 782 | { name = 'VCR OSD Mono', source = 'VCR OSD Mono' }, 783 | { name = 'Webdings', source = 'Webdings:@System' }, 784 | } 785 | for _, v in ipairs(io.scanDir(fontsDir, '*.ttf')) do 786 | table.insert(fonts, { name = v:sub(1, #v - 4), source = v:sub(1, #v - 4)..':'..__dirname..'/fonts' }) 787 | end 788 | table.sort(fonts, function (a, b) return a.name < b.name end) 789 | end 790 | 791 | local tools = { 792 | { 793 | name = 'Brush (B)', 794 | key = shortcuts.toolBrush, 795 | icon = icons.Brush, 796 | ui = function (s) 797 | ui.header('Color:') 798 | ColorBlock() 799 | ui.offsetCursorY(20) 800 | ui.header('Brush:') 801 | BrushBlock(s.brush) 802 | BrushBaseBlock(s.brush, 0.5) 803 | end, 804 | brush = brushParams('brush'), 805 | brushColor = function(s) return rgbm.new(stored.color.rgb, s.brush.brushAlpha) end, 806 | brushSize = function (s) return vec2(s.brush.brushSize, s.brush.brushSize) end, 807 | -- blendMode = render.BlendMode.BlendAccurate, 808 | }, 809 | { 810 | name = 'Eraser (E)', 811 | key = shortcuts.toolEraser, 812 | icon = icons.Eraser, 813 | ui = function (s) 814 | ui.header('Background color:') 815 | ColorBlock('bgColor') 816 | ui.offsetCursorY(20) 817 | ui.header('Eraser:') 818 | BrushBlock(s.brush) 819 | BrushBaseBlock(s.brush, 0.5) 820 | end, 821 | brush = brushParams('eraser'), 822 | brushColor = function(s) return stored.bgColor end, 823 | brushSize = function (s) return vec2(s.brush.brushSize, s.brush.brushSize) end, 824 | }, 825 | { 826 | name = 'Stamp (S)', 827 | key = shortcuts.toolStamp, 828 | icon = icons.Stamp, 829 | ui = function (s) 830 | ui.header('Color:') 831 | ColorBlock() 832 | ui.offsetCursorY(20) 833 | 834 | ui.header('Stamp:') 835 | ui.combo('##set', string.format('Set: %s', selectedStickerSet.name), ui.ComboFlags.None, function () 836 | for i = 1, #stickers do 837 | if ui.selectable(stickers[i].name, stickers[i] == selectedStickerSet) then 838 | selectedStickerSet = stickers[i] 839 | stored.selectedStickerSet = i 840 | end 841 | end 842 | if ui.selectable('New category…') then 843 | ui.modalPrompt('Create new category', 'Category name:', nil, function (value) 844 | if #value > 0 and io.createDir(decalsDir..'/'..value) then 845 | ui.toast(ui.Icons.Confirm, 'New category created: '..tostring(value)) 846 | rescanStickers() 847 | selectedStickerSet = table.findFirst(stickers, function (item) return item.name == value end) 848 | else 849 | ui.toast(ui.Icons.Warning, 'Couldn’t create a new category: '..tostring(value)) 850 | end 851 | end) 852 | end 853 | end) 854 | 855 | local items = selectedStickerSet.items 856 | if s.brush.brushTex == '' then s.brush.brushTex = items[1][2] end 857 | ui.childWindow('stickersList', vec2(210, 210), false, ui.WindowFlags.AlwaysVerticalScrollbar, function () 858 | ui.pushStyleColor(ui.StyleColor.Button, rgbm.colors.transparent) 859 | local itemSize = vec2(100, 60) 860 | for i = 1, #items do 861 | if ui.areaVisible(itemSize) then 862 | local size = ui.imageSize(items[i][2]) 863 | if ui.button('##'..i, vec2(100, 60), s.brush.brushTex == items[i][2] and ui.ButtonFlags.Active or ui.ButtonFlags.None) then 864 | s.brush.brushTex = items[i][2] 865 | selectedBrushOutlineDirty = true 866 | end 867 | local s = vec2(90, 90 * size.y / size.x) 868 | if s.y > 54 then s:scale(54 / s.y) end 869 | ui.addIcon(items[i][2], s, 0.5, nil, 0) 870 | if ui.itemHovered() then ui.setTooltip('Brush: '..items[i][1]) end 871 | else 872 | ui.dummy(itemSize) 873 | end 874 | if i % 2 == 1 then ui.sameLine(0, 0) end 875 | end 876 | ui.popStyleColor() 877 | ui.newLine() 878 | end) 879 | 880 | local _, i = table.findFirst(items, function (item, _, tex) 881 | return item[2] == tex 882 | end, s.brush.brushTex) 883 | i = i or 0 884 | 885 | if shortcuts.arrowRight() then 886 | s.brush.brushTex = items[i % #items + 1][2] 887 | selectedBrushOutlineDirty = true 888 | end 889 | 890 | if shortcuts.arrowDown() then 891 | s.brush.brushTex = items[(i + 1) % #items + 1][2] 892 | selectedBrushOutlineDirty = true 893 | end 894 | 895 | if shortcuts.arrowLeft() then 896 | s.brush.brushTex = items[(i - 2 + #items) % #items + 1][2] 897 | selectedBrushOutlineDirty = true 898 | end 899 | 900 | if shortcuts.arrowUp() then 901 | s.brush.brushTex = items[(i - 3 + #items) % #items + 1][2] 902 | selectedBrushOutlineDirty = true 903 | end 904 | 905 | if ui.itemHovered() then 906 | ui.setTooltip('Use arrow keys to quickly switch between items') 907 | end 908 | 909 | ui.itemPopup(function () 910 | if ui.selectable('Add new decal…') then 911 | os.openFileDialog({ 912 | title = 'Add new decal', 913 | defaultFolder = ac.getFolder(ac.FolderID.Root), 914 | fileTypes = { { name = 'Images', mask = '*.png' } }, 915 | addAllFilesFileType = true, 916 | flags = bit.bor(os.DialogFlags.PathMustExist, os.DialogFlags.FileMustExist) 917 | }, function (err, filename) 918 | if filename then 919 | local fileName = filename:gsub('.+[/\\\\]', '') 920 | if io.copyFile(filename, decalsDir..'/'..selectedStickerSet.name..'/'..fileName, true) then 921 | rescanStickers() 922 | selectedStickerSet = table.findFirst(stickers, function (item) return item.name == selectedStickerSet.name end) 923 | s.brush.brushTex = decalsDir..'/'..selectedStickerSet.name..'/'..fileName 924 | ui.toast(ui.Icons.Confirm, 'New decal added: '..fileName:sub(1, #fileName - 4)) 925 | return 926 | end 927 | end 928 | if err or filename then 929 | ui.toast(ui.Icons.Warning, 'Couldn’t add a new decal: '..(err or 'unknown error')) 930 | end 931 | end) 932 | end 933 | if ui.selectable('Open in Explorer') then 934 | os.openInExplorer(decalsDir) 935 | end 936 | if ui.selectable('Refresh') then 937 | rescanStickers() 938 | end 939 | end) 940 | 941 | ui.alignTextToFramePadding() 942 | ui.text('Align sticker:') 943 | ui.sameLine() 944 | ui.setNextItemWidth(ui.availableSpaceX()) 945 | stored.alignSticker = ui.combo('##alignSticker', stored.alignSticker, ui.ComboFlags.None, { 946 | 'No', 947 | 'Align to surface', 948 | 'Fully align' 949 | }) 950 | 951 | local brush = s.brush 952 | BrushBaseBlock(brush, 4, true) 953 | end, 954 | brush = brushParams('stamp', 0.2, 1), 955 | brushColor = function(s) return rgbm.new(stored.color.rgb, s.brush.brushAlpha) end, 956 | brushSize = function (s) 957 | local size = ui.imageSize(s.brush.brushTex) 958 | return vec2(s.brush.brushSize, s.brush.brushSize * size.y / size.x) 959 | end, 960 | stickerMode = true, 961 | stickerContinious = false, 962 | }, 963 | { 964 | name = 'Mirroring stamp (K)', 965 | key = shortcuts.toolMirroringStamp, 966 | icon = icons.MirroringStamp, 967 | ui = function (s) 968 | ui.header('Mirroring stamp:') 969 | BrushBlock(s.brush) 970 | BrushBaseBlock(s.brush, 0.5, false, true, true) 971 | end, 972 | brush = brushParams('mirroringStamp'), 973 | procBrushTex = function (s, ray, previewMode) 974 | if not s._shot then 975 | s._shot = ac.GeometryShot(selectedMeshes, 256):setShadersType(render.ShadersType.SampleColor) 976 | s._ksAmbient = selectedMeshes:getMaterialPropertyValue('ksAmbient') 977 | end 978 | local up = getBrushUp(ray.dir, s) 979 | selectedMeshes:setMaterialTexture('txDiffuse', editingCanvas) 980 | selectedMeshes:setMaterialProperty('ksAmbient', 1) 981 | s._shot:clear(table.random(rgbm.colors)) 982 | local lpos, ldir, lup = car.worldToLocal:transformPoint(ray.pos), car.worldToLocal:transformVector(ray.dir), car.worldToLocal:transformVector(up) 983 | lpos.x, ldir.x, lup.x = -lpos.x, -ldir.x, -lup.x 984 | local ipos, idir, iup = car.bodyTransform:transformPoint(lpos), car.bodyTransform:transformVector(ldir), car.bodyTransform:transformVector(lup) 985 | local brushSize = previewMode and s.brush.brushSize or s.brush.brushSize * brushSizeMult(s.brush) 986 | s._shot:setOrthogonalParams(vec2(brushSize, brushSize), 100):update(ipos, idir, iup, 0) 987 | selectedMeshes:setMaterialTexture('txDiffuse', aoCanvas) 988 | selectedMeshes:setMaterialProperty('ksAmbient', s._ksAmbient) 989 | -- DebugTex = s._shot 990 | return s._shot 991 | end, 992 | procProjParams = function (s, pr) 993 | pr.mask2 = s.brush.brushTex 994 | pr.mask2Flags = render.TextureMaskFlags.UseAlpha 995 | end, 996 | brushColor = function(s) return rgbm(1, 1, 1, s.brush.brushAlpha) end, 997 | brushSize = function (s) return vec2(-s.brush.brushSize, s.brush.brushSize) end, 998 | stickerMode = true, 999 | stickerNoAlignment = true, 1000 | stickerContinious = true 1001 | }, 1002 | { 1003 | name = 'Blur/Smudge (Alt+B)', 1004 | key = shortcuts.toolBlurTool, 1005 | icon = icons.BlurTool, 1006 | ui = function (s) 1007 | ui.header('Blur tool:') 1008 | BrushBlock(s.brush) 1009 | BrushBaseBlock(s.brush, 0.5, false, true, true) 1010 | 1011 | s.brush.blur = ui.slider('##blur', s.brush.blur * 1000, 0, 100, 'Blur: %.0f%%') / 1000 1012 | s.brush.smudge = ui.slider('##smudge', s.brush.smudge * 100, 0, 100, 'Smudge: %.0f%%', 0.5) / 100 1013 | 1014 | ui.offsetCursorY(20) 1015 | ui.header('Sharpness boost:') 1016 | if ui.checkbox('Active', s.brush.sharpnessMode) then 1017 | s.brush.sharpnessMode = not s.brush.sharpnessMode 1018 | end 1019 | s.brush.sharpness = ui.slider('##sharpness', s.brush.sharpness * 100, 0, 500, 'Intensity: %.0f%%', 2) / 100 1020 | ui.textWrapped('Sharpness boost is some sort of an inverse to blur. Might help to increase local sharpness a bit or, with less well tuned settings, achieve some other strange effects.') 1021 | end, 1022 | brush = brushParams('blurTool', nil, nil, { blur = 0.01, smudge = 0, sharpnessMode = false, sharpness = 1.5 }), 1023 | procBrushTex = function (s, ray, previewMode) 1024 | if not s._shot then 1025 | s._shot = ac.GeometryShot(selectedMeshes, 256):setShadersType(render.ShadersType.SampleColor) 1026 | s._shotBlurred = ui.ExtraCanvas(vec2(128, 128)) 1027 | s._shotSharpened = ui.ExtraCanvas(vec2(128, 128)) 1028 | s._ksAmbient = selectedMeshes:getMaterialPropertyValue('ksAmbient') 1029 | end 1030 | if previewMode or not s._rayPos then 1031 | s._rayPos = ray.pos:clone() 1032 | s._rayDir = ray.dir:clone() 1033 | if previewMode then return end 1034 | else 1035 | s._rayPos = math.applyLag(s._rayPos, ray.pos, s.brush.smudge, ac.getDeltaT()) 1036 | s._rayDir = math.applyLag(s._rayDir, ray.dir, s.brush.smudge, ac.getDeltaT()):normalize() 1037 | end 1038 | local up = getBrushUp(s._rayDir, s) 1039 | selectedMeshes:setMaterialTexture('txDiffuse', editingCanvas) 1040 | selectedMeshes:setMaterialProperty('ksAmbient', 1) 1041 | s._shot:clear(table.random(rgbm.colors)) 1042 | local brushSize = s.brush.brushSize * brushSizeMult(s.brush) 1043 | s._shot:setOrthogonalParams(vec2(brushSize, brushSize), 100):update(s._rayPos, s._rayDir, up, 0) 1044 | selectedMeshes:setMaterialTexture('txDiffuse', aoCanvas) 1045 | selectedMeshes:setMaterialProperty('ksAmbient', s._ksAmbient) 1046 | if s.brush.blur <= 0.0001 then 1047 | return s._shot 1048 | end 1049 | s._shotBlurred:clear(rgbm.colors.transparent):update(function (dt) 1050 | ui.beginBlurring() 1051 | ui.drawImage(s._shot, 0, 128) 1052 | ui.endBlurring(s.brush.blur) 1053 | end) 1054 | if s.brush.sharpnessMode then 1055 | s._shotSharpened:update(function (dt) 1056 | ui.renderShader({ 1057 | p1 = vec2(0, 0), 1058 | p2 = vec2(128, 128), 1059 | blendMode = render.BlendMode.Opaque, 1060 | textures = { 1061 | txBlurred = s._shotBlurred, 1062 | txBase = s._shot 1063 | }, 1064 | values = { 1065 | gIntensity = tonumber(s.brush.sharpness) 1066 | }, 1067 | shader = [[float4 main(PS_IN pin) { 1068 | float4 r = lerp(txBlurred.Sample(samLinear, pin.Tex), txBase.Sample(samLinear, pin.Tex), gIntensity); 1069 | r.a = 1; 1070 | return r; 1071 | }]] 1072 | }) 1073 | end) 1074 | return s._shotSharpened 1075 | end 1076 | return s._shotBlurred 1077 | end, 1078 | procProjParams = function (s, pr) 1079 | pr.mask2 = s.brush.brushTex 1080 | pr.mask2Flags = render.TextureMaskFlags.UseAlpha 1081 | end, 1082 | brushColor = function(s) return rgbm(1, 1, 1, s.brush.brushAlpha) end, 1083 | brushSize = function (s) return vec2(s.brush.brushSize, s.brush.brushSize) end, 1084 | stickerMode = true, 1085 | stickerNoAlignment = true, 1086 | stickerContinious = true 1087 | }, 1088 | { 1089 | name = 'Text (T)', 1090 | key = shortcuts.toolText, 1091 | icon = icons.Text, 1092 | ui = function (s) 1093 | if fonts == nil then 1094 | rescanFonts() 1095 | end 1096 | 1097 | local selectedFont = table.findFirst(fonts, function (item, _, sf) return item.source == sf end, stored.selectedFont) 1098 | if selectedFont == nil then 1099 | selectedFont = fonts[1] 1100 | stored.selectedFont = selectedFont.source 1101 | end 1102 | 1103 | ui.header('Color:') 1104 | ColorBlock() 1105 | ui.offsetCursorY(20) 1106 | 1107 | ui.beginGroup() 1108 | ui.header('Text:') 1109 | s._labelText = ui.inputText('Text', s._labelText, ui.InputTextFlags.Placeholder) 1110 | if ui.itemEdited() then s._labelDirty = true end 1111 | 1112 | ui.combo('##fonts', 'Font: '..tostring(selectedFont.name), ui.ComboFlags.None, function () 1113 | for i = 1, #fonts do 1114 | if ui.selectable(fonts[i].name, fonts[i] == selectedFont) then 1115 | selectedFont = fonts[i] 1116 | stored.selectedFont, s._labelDirty = selectedFont.source, true 1117 | end 1118 | 1119 | if ui.itemHovered() then 1120 | ui.tooltip(function () 1121 | if s._previewCanvas ~= nil then 1122 | s._previewCanvas:dispose() 1123 | end 1124 | 1125 | local font = fonts[i].source 1126 | if stored.fontBold then font = font..';Weight=Bold' end 1127 | if stored.fontItalic then font = font..';Style=Italic' end 1128 | ui.pushDWriteFont(font) 1129 | local canvasSize = ui.measureDWriteText(s._labelText, 24) 1130 | canvasSize.x, canvasSize.y = math.max(canvasSize.x, 24), canvasSize.y + 8 1131 | s._previewCanvas = ui.ExtraCanvas(canvasSize):clear(rgbm.colors.transparent):update(function (dt) 1132 | ui.dwriteTextAligned(s._labelText, 24, ui.Alignment.Center, ui.Alignment.Center, ui.availableSpace(), false, rgbm.colors.white) 1133 | end) 1134 | ui.popDWriteFont() 1135 | ui.image(s._previewCanvas, canvasSize) 1136 | end) 1137 | end 1138 | end 1139 | end) 1140 | ui.itemPopup(function () 1141 | if ui.selectable('Open in Explorer') then 1142 | os.openInExplorer(fontsDir) 1143 | end 1144 | if ui.selectable('Refresh') then 1145 | rescanFonts() 1146 | end 1147 | end) 1148 | 1149 | if ui.checkbox('Bold', stored.fontBold) then stored.fontBold, s._labelDirty = not stored.fontBold, true end 1150 | if ui.checkbox('Italic', stored.fontItalic) then stored.fontItalic, s._labelDirty = not stored.fontItalic, true end 1151 | ui.endGroup() 1152 | 1153 | if ui.itemHovered() and not s._labelDirty then 1154 | ui.tooltip(function () 1155 | ui.image(s.brush.brushTex, ui.imageSize(s.brush.brushTex):scale(0.5)) 1156 | end) 1157 | end 1158 | 1159 | -- local size = ui.imageSize(s.brush.brushTex) 1160 | -- ui.drawImage(s.brush.brushTex, ui.getCursor(), ui.getCursor() + vec2(210, 210 * size.y / size.x)) 1161 | -- ui.offsetCursorY(math.ceil(210 * size.y / size.x / 20 + 0.5) * 20) 1162 | 1163 | ui.alignTextToFramePadding() 1164 | ui.text('Align text:') 1165 | ui.sameLine() 1166 | ui.setNextItemWidth(ui.availableSpaceX()) 1167 | stored.alignSticker = ui.combo('##alignSticker', stored.alignSticker, ui.ComboFlags.None, { 1168 | 'No', 1169 | 'Align to surface', 1170 | 'Fully align' 1171 | }) 1172 | 1173 | local brush = s.brush 1174 | BrushBaseBlock(brush, 4, true) 1175 | 1176 | if s._labelDirty then 1177 | if s.brush.brushTex and type(s.brush.brushTex) ~= 'string' then 1178 | s.brush.brushTex:dispose() 1179 | end 1180 | local font = selectedFont.source 1181 | if stored.fontBold then font = font..';Weight=Bold' end 1182 | if stored.fontItalic then font = font..';Style=Italic' end 1183 | ui.pushDWriteFont(font) 1184 | local canvasSize = ui.measureDWriteText(s._labelText, 48) 1185 | canvasSize.x, canvasSize.y = math.max(canvasSize.x, 48), canvasSize.y + 16 1186 | s.brush.brushTex = ui.ExtraCanvas(canvasSize):clear(rgbm.colors.transparent):update(function (dt) 1187 | ui.dwriteTextAligned(s._labelText, 48, ui.Alignment.Center, ui.Alignment.Center, ui.availableSpace(), false, rgbm.colors.white) 1188 | end) 1189 | ui.popDWriteFont() 1190 | s._labelDirty = false 1191 | end 1192 | end, 1193 | brush = brushParams('text', 0.2, 1), 1194 | brushColor = function(s) return rgbm.new(stored.color.rgb, s.brush.brushAlpha) end, 1195 | brushSize = function (s) 1196 | local size = ui.imageSize(s.brush.brushTex) 1197 | return vec2(s.brush.brushSize, s.brush.brushSize * size.y / size.x) 1198 | end, 1199 | stickerMode = true, 1200 | stickerContinious = false, 1201 | blendMode = render.BlendMode.BlendPremultiplied, 1202 | _labelText = ac.getDriverName(0), 1203 | _labelDirty = true 1204 | }, 1205 | { 1206 | name = 'Masking (M)', 1207 | key = shortcuts.toolMasking, 1208 | icon = icons.Masking, 1209 | ui = function (s) 1210 | -- if not maskingCarView then 1211 | -- maskingCarView = ac.GeometryShot(selectedMeshes, vec2(210, 130)):setClippingPlanes(100, 1e5) 1212 | -- selectedMeshes:setMaterialTexture('txDiffuse', maskingCanvas) 1213 | -- maskingCarView:update(car.position + car.side * 1000, -car.side, car.up, 0.15) 1214 | -- selectedMeshes:setMaterialTexture('txDiffuse', aoCanvas) 1215 | -- end 1216 | -- ui.drawImage(maskingCarView, ui.getCursor(), ui.getCursor() + vec2(210, 130)) 1217 | 1218 | if ui.checkbox('Masking is active', maskingActive) then 1219 | maskingActive = not maskingActive 1220 | end 1221 | if ui.itemHovered() then 1222 | ui.setTooltip('Toggle masking (Ctrl+M)') 1223 | end 1224 | 1225 | -- ui.textWrapped('Masking tool is a plane separating model in two halves. When you draw a thing, it would only get drawn on the side of a plane with camera. Might help in masking things quickly. For something more complex, use stencils.\n\nClick model and drag mouse to quickly create a new plane.') 1226 | ui.textWrapped('Masking tool is a plane separating model in two halves. When you draw a thing, it would only get drawn on the side of a plane with camera. Might help in masking things quickly.\n\nClick model and drag mouse to quickly create a new plane.\n\nPro tip: when using brush, hold M for more than 0.2 seconds: tool will switch to masking temporary, so you can quickly put a mask and go back to brush by releasing M.') 1227 | end, 1228 | action = function (s) 1229 | local ray = render.createMouseRay() 1230 | local d = selectedMeshes:raycast(ray) 1231 | if d ~= -1 then s._d = d end 1232 | if d ~= -1 and uiState.isMouseLeftKeyClicked then 1233 | maskingCreatingFrom = car.worldToLocal:transformPoint(ray.pos + ray.dir * d) 1234 | s._moving = false 1235 | elseif maskingCreatingFrom then 1236 | if not uiState.isMouseLeftKeyDown then 1237 | if s._moving then 1238 | local endingPos = car.worldToLocal:transformPoint(ray.pos + ray.dir * d) 1239 | applyQuickMasking(maskingCreatingFrom, endingPos) 1240 | s._moving = false 1241 | end 1242 | maskingCreatingFrom, maskingCreatingTo = nil, nil 1243 | end 1244 | if not s._moving and #ui.mouseDragDelta() > 0 then 1245 | addUndo(maskingBackup()) 1246 | s._moving = true 1247 | maskingActive = true 1248 | end 1249 | if s._moving then 1250 | maskingCreatingTo = car.worldToLocal:transformPoint(ray.pos + ray.dir * s._d) 1251 | end 1252 | end 1253 | end, 1254 | }, 1255 | { 1256 | name = 'Eyedropper (I)', 1257 | key = shortcuts.toolEyeDropper, 1258 | icon = icons.EyeDropper, 1259 | ui = function (s) 1260 | ui.header('Color:') 1261 | ColorBlock() 1262 | ui.offsetCursorY(20) 1263 | 1264 | ui.header('Eyedropper:') 1265 | ui.alignTextToFramePadding() 1266 | ui.text('Sample size:') 1267 | ui.sameLine() 1268 | ui.setNextItemWidth(ui.availableSpaceX()) 1269 | stored.eyeDropperRange = ui.combo('##sampleSize', stored.eyeDropperRange, ui.ComboFlags.None, { 1270 | 'Point sample', 1271 | '3 by 3 average', 1272 | '5 by 5 average', 1273 | '7 by 7 average', 1274 | '9 by 9 average', 1275 | }) 1276 | if s._color and not ui.mouseBusy() then 1277 | ColorTooltip(s._color) 1278 | if uiState.isMouseLeftKeyDown then 1279 | stored.color = s._color 1280 | s._changing = true 1281 | elseif s._changing then 1282 | s._changing = false 1283 | palette.addToUserPalette(s._color) 1284 | end 1285 | end 1286 | end, 1287 | action = function (s) 1288 | if accessibleData ~= nil then 1289 | local ray = render.createMouseRay() 1290 | local uv = vec2() 1291 | if selectedMeshes:raycast(ray, false, nil, nil, uv) ~= -1 then 1292 | uv.x = uv.x - math.floor(uv.x) 1293 | uv.y = uv.y - math.floor(uv.y) 1294 | local c = uv * accessibleData:size() 1295 | local range = 1 + (stored.eyeDropperRange - 1) * 2 1296 | local offset = -math.ceil(range / 2) 1297 | local cx, cy = math.floor(c.x) + offset, math.floor(c.y) + offset 1298 | local colorPick = rgbm() 1299 | s._color:set(colorPick) 1300 | for x = 1, range do 1301 | for y = 1, range do 1302 | s._color:add(accessibleData:colorTo(colorPick, cx + x, cy + y)) 1303 | end 1304 | end 1305 | s._color:scale(1 / (range * range)) 1306 | end 1307 | end 1308 | end, 1309 | _color = rgbm(1, 1, 1, 1), 1310 | _changing = false 1311 | } 1312 | } 1313 | 1314 | local activeTool = tools[stored.activeToolIndex] 1315 | local previousToolIndex = stored.activeToolIndex 1316 | local toolSwitched = 0 1317 | 1318 | local function SkinEditor() 1319 | DrawControl() 1320 | if selectedMeshes == nil then return end 1321 | ui.offsetCursorY(20) 1322 | 1323 | ui.header('Tools:') 1324 | for i = 1, #tools do 1325 | local v = tools[i] 1326 | local s = activeTool == v and toolSwitched ~= 0 and ui.time() > toolSwitched + 0.2 1327 | local bg = s and rgbm(0.5, 0.5, 0, 1) or activeTool == v and uiState.accentColor * rgbm(1, 1, 1, 0.5) 1328 | if bg then ui.pushStyleColor(ui.StyleColor.Button, bg) end 1329 | if IconButton(v.icon, v.name, activeTool == v) or v.key and v.key(false) then 1330 | activeTool = v 1331 | toolSwitched = v.key and tonumber(ui.time()) or 0 1332 | previousToolIndex = stored.activeToolIndex 1333 | stored.activeToolIndex = i 1334 | selectedBrushOutlineDirty = true 1335 | end 1336 | if bg then ui.popStyleColor() end 1337 | ui.sameLine(0, 4) 1338 | if ui.availableSpaceX() < 12 then ui.newLine(4) end 1339 | end 1340 | if IconButton(icons.Camera, 'Orbit camera (Ctrl+Space)\nUse middle mouse button or hold space to rotate camera', stored.orbitCamera) or shortcuts.toggleOrbitCamera() then 1341 | stored.orbitCamera = not stored.orbitCamera 1342 | end 1343 | ui.sameLine(0, 4) 1344 | if ui.availableSpaceX() < 32 then ui.newLine(4) end 1345 | if IconButton(icons.MirroringHelper, 'Project other side (Ctrl+E)\nProject other side on current side to make making things symmetrical easier', stored.projectOtherSide) or shortcuts.toggleProjectOtherSide() then 1346 | stored.projectOtherSide = not stored.projectOtherSide 1347 | end 1348 | 1349 | ui.offsetCursorY(20) 1350 | 1351 | if toolSwitched ~= 0 and not activeTool.key:down() then 1352 | if ui.time() > toolSwitched + 0.2 then 1353 | activeTool = tools[previousToolIndex] 1354 | toolSwitched = 0 1355 | stored.activeToolIndex = previousToolIndex 1356 | selectedBrushOutlineDirty = true 1357 | else 1358 | toolSwitched = 0 1359 | end 1360 | end 1361 | 1362 | if shortcuts.toggleMasking() then 1363 | maskingActive = not maskingActive 1364 | end 1365 | 1366 | ui.pushID(activeTool.name) 1367 | ui.pushFont(ui.Font.Small) 1368 | activeTool:ui() 1369 | ui.popFont() 1370 | ui.popID() 1371 | end 1372 | 1373 | local pdistance, pnormal, pdir = 1, vec3(), vec3(1, 0, 0) 1374 | 1375 | local function projectBrushTexture(tex, pos, dir, color, distance, previewMode, doNotUseToolProjParams) 1376 | local brush = activeTool.brush 1377 | if not brush then return end 1378 | 1379 | if activeTool.stickerMode and not activeTool.stickerNoAlignment and stored.alignSticker > 1 then 1380 | local d, m = selectedMeshes:raycast(render.createRay(pos, dir), true, nil, pnormal) 1381 | if d ~= -1 then 1382 | pdir = m:getWorldTransformationRaw():transformVector(pnormal):scale(-1) 1383 | pdistance = d 1384 | else 1385 | d = pdistance 1386 | end 1387 | pos = pos + dir * d 1388 | dir = pdir:clone() 1389 | if stored.alignSticker == 3 then 1390 | dir = dir - car.up * dir:dot(car.up) 1391 | end 1392 | distance = 0.2 1393 | end 1394 | 1395 | local size = activeTool:brushSize() 1396 | if not previewMode and (not activeTool.stickerMode or activeTool.stickerContinious) then size = size * brushSizeMult(brush) end 1397 | if brush.brushAspectMult > 1 then size.x = size.x * brush.brushAspectMult 1398 | else size.y = size.y / brush.brushAspectMult end 1399 | if brush.brushMirror then 1400 | size.x = -size.x 1401 | end 1402 | if not activeTool.__brushRandomAngle or previewMode then 1403 | activeTool.__brushRandomAngle = activeTool.brush.brushAngle 1404 | else 1405 | activeTool.__brushRandomAngle = math.random() * 360 1406 | end 1407 | local up = getBrushUp(dir, activeTool) 1408 | local pr = { 1409 | filename = tex, 1410 | pos = pos, 1411 | look = dir, 1412 | up = up, 1413 | color = color, 1414 | size = size, 1415 | depth = brush.paintThrough and 1e9 or distance, 1416 | doubleSided = brush.paintThrough, 1417 | mask1 = maskingCanvas, 1418 | mask1Flags = bit.bor(render.TextureMaskFlags.AltUV, render.TextureMaskFlags.Default), 1419 | blendMode = not previewMode and activeTool.blendMode or nil 1420 | } 1421 | if activeTool.procProjParams and not doNotUseToolProjParams then activeTool:procProjParams(pr) end 1422 | selectedMeshes:projectTexture(pr) 1423 | if brush.withMirror then 1424 | local lpos, ldir, lup = car.worldToLocal:transformPoint(pos), car.worldToLocal:transformVector(dir), car.worldToLocal:transformVector(up) 1425 | lpos.x, ldir.x, lup.x = -lpos.x, -ldir.x, -lup.x 1426 | pr.pos, pr.look, pr.up = car.bodyTransform:transformPoint(lpos), car.bodyTransform:transformVector(ldir), car.bodyTransform:transformVector(lup) 1427 | pr.size.x = -pr.size.x 1428 | selectedMeshes:projectTexture(pr) 1429 | end 1430 | end 1431 | 1432 | local function updateBrushOutline(stickerMode) 1433 | if not selectedBrushOutline then 1434 | selectedBrushOutline = ui.ExtraCanvas(vec2(128, 128), 4) 1435 | end 1436 | selectedBrushOutlineDirty = false 1437 | if not activeTool.brush or stickerMode then 1438 | selectedBrushOutline:clear(rgbm.colors.transparent) 1439 | return 1440 | end 1441 | -- prepare brush outline in two stages: first, boost alpha and draw brush in white and draw it 1442 | -- again in black and smaller to get a black and white mask, and then draw that mask with different 1443 | -- shading params to turn black and white mask into transparency 1444 | selectedBrushOutline:clear(rgbm.colors.black) 1445 | selectedBrushOutline:update(function (dt) 1446 | ui.renderShader({ 1447 | p1 = vec2(0, 0), 1448 | p2 = vec2(128, 128), 1449 | blendMode = render.BlendMode.Opaque, 1450 | textures = { 1451 | txBrush = activeTool.brush.brushTex 1452 | }, 1453 | values = { 1454 | gMargin = (0.5/128) / activeTool.brush.brushSize 1455 | }, 1456 | shader = [[float4 main(PS_IN pin) { 1457 | float tx = txBrush.Sample(samLinearBorder0, pin.Tex + float2(gMargin, gMargin)).w 1458 | + txBrush.Sample(samLinearBorder0, pin.Tex + float2(gMargin, -gMargin)).w 1459 | + txBrush.Sample(samLinearBorder0, pin.Tex + float2(-gMargin, gMargin)).w 1460 | + txBrush.Sample(samLinearBorder0, pin.Tex + float2(-gMargin, -gMargin)).w; 1461 | tx = saturate(tx * 20 - 1); 1462 | tx *= 1 - saturate(txBrush.Sample(samLinear, pin.Tex).w * 20 - 1); 1463 | return float4(1, 1, 1, tx); 1464 | }]] 1465 | }) 1466 | end) 1467 | end 1468 | 1469 | local otherSideShot ---@type ac.GeometryShot 1470 | local otherSidePhase = -1 1471 | local otherSideSide = 0 1472 | local bakKsAmbient 1473 | 1474 | local function updateAOCanvas() 1475 | if aoCanvas == nil then return end 1476 | 1477 | local projectDir 1478 | if stored.projectOtherSide then 1479 | if not otherSideShot then 1480 | bakKsAmbient = selectedMeshes:getMaterialPropertyValue('ksAmbient') 1481 | otherSideShot = ac.GeometryShot(selectedMeshes, 2048):setOrthogonalParams(vec2(6, 4), 10):setClippingPlanes(-10, 0):setShadersType(render.ShadersType.SampleColor) 1482 | end 1483 | 1484 | projectDir = car.side 1485 | local s = math.sign(projectDir:dot(ac.getCameraForward())) 1486 | if s > 0 then projectDir = -projectDir end 1487 | if s ~= otherSideSide then otherSidePhase, otherSideSide = -1, s end 1488 | 1489 | if otherSidePhase ~= editingCanvasPhase then 1490 | otherSidePhase = editingCanvasPhase 1491 | selectedMeshes:setMaterialTexture('txDiffuse', editingCanvas) 1492 | selectedMeshes:setMaterialProperty('ksAmbient', 1) 1493 | otherSideShot:update(car.position, projectDir, car.up, 0) 1494 | selectedMeshes:setMaterialTexture('txDiffuse', aoCanvas) 1495 | selectedMeshes:setMaterialProperty('ksAmbient', bakKsAmbient) 1496 | end 1497 | end 1498 | 1499 | local ray, tex 1500 | ray = render.createMouseRay() 1501 | if activeTool.stickerMode then 1502 | if activeTool.procBrushTex then tex = activeTool:procBrushTex(ray, true) 1503 | else tex = activeTool.brush.brushTex end 1504 | end 1505 | 1506 | if selectedBrushOutlineDirty then 1507 | updateBrushOutline(activeTool.stickerMode and tex ~= nil) 1508 | end 1509 | 1510 | aoCanvas:update(function (dt) 1511 | drawWithAO(editingCanvas, aoTexture or carTexture) 1512 | 1513 | if stored.projectOtherSide then 1514 | selectedMeshes:projectTexture({ 1515 | filename = otherSideShot, 1516 | pos = car.position, 1517 | look = -projectDir, 1518 | up = car.up, 1519 | color = rgbm(1, 1, 1, 0.1), 1520 | size = vec2(-6, 4), 1521 | depth = 1e9, 1522 | doubleSided = false 1523 | }) 1524 | end 1525 | 1526 | if tex then 1527 | projectBrushTexture(tex, ray.pos, ray.dir, activeTool:brushColor() * rgbm(1, 1, 1, 0.3), nil, true) 1528 | else 1529 | projectBrushTexture(selectedBrushOutline, ray.pos, ray.dir, rgbm.colors.gray, nil, true, activeTool.stickerMode) 1530 | end 1531 | end) 1532 | end 1533 | 1534 | local maskingDirty = true 1535 | 1536 | local function updateMaskingCanvas() 1537 | if not maskingActive then 1538 | if maskingCanvas and maskingDirty then 1539 | maskingDirty = false 1540 | maskingCanvas:clear(rgbm.colors.white) 1541 | end 1542 | return 1543 | end 1544 | 1545 | if not maskingCanvas then 1546 | maskingCanvas = ui.ExtraCanvas(vec2(2048, 2048)) 1547 | end 1548 | 1549 | maskingDirty = true 1550 | maskingCanvas:clear(rgbm.colors.black) 1551 | maskingCanvas:update(function (dt) 1552 | local mdir = maskingDir 1553 | if mdir:dot(car.worldToLocal:transformPoint(ac.getCameraPosition()) - maskingPos) < 0 then mdir = mdir:clone():scale(-1) end 1554 | local pos = maskingPos + mdir * 5 1555 | local dir = math.cross(mdir, vec3(0, 0, 1)) 1556 | selectedMeshes:projectTexture({ 1557 | filename = 'color::#ffffff', 1558 | pos = car.bodyTransform:transformPoint(pos), 1559 | look = car.bodyTransform:transformVector(dir), 1560 | up = car.bodyTransform:transformVector(mdir), 1561 | color = rgbm.colors.white, 1562 | size = vec2(10, 10), 1563 | depth = 1e9, 1564 | doubleSided = true 1565 | }) 1566 | end) 1567 | end 1568 | 1569 | local function cameraUpdate() 1570 | if editingCanvas == nil then 1571 | editingCanvas = ui.ExtraCanvas(vec2(2048, 2048)):clear(rgbm.new(stored.bgColor.rgb, 1)) 1572 | aoCanvas = ui.ExtraCanvas(vec2(2048, 2048), 4, render.AntialiasingMode.CMAA) 1573 | selectedMeshes:setMaterialTexture('txDiffuse', aoCanvas) 1574 | end 1575 | 1576 | if camera then 1577 | local mat = mat4x4.rotation(cameraAngle.y, vec3(1, 0, 0)):mul(mat4x4.rotation(cameraAngle.x, vec3(0, 1, 0))):mul(car.bodyTransform) 1578 | camera.transform.position = mat:transformPoint(vec3(0, car.aabbCenter.y * math.smoothstep(math.lerpInvSat(cameraAngle.y, 0.5, 0)), -8)) 1579 | camera.transform.look = mat:transformVector(vec3(0, 0, 1)) 1580 | camera.transform.up = mat:transformVector(vec3(0, 1, 0)) 1581 | camera.fov = 24 1582 | 1583 | camera.ownShare = math.applyLag(camera.ownShare, stored.orbitCamera and 1 or 0, 0.85, ac.getDeltaT()) 1584 | if stored.orbitCamera and (ui.keyboardButtonDown(ui.KeyIndex.Space) or ui.mouseDown(ui.MouseButton.Middle)) then 1585 | cameraAngle:add(uiState.mouseDelta * vec2(-0.003, 0.003)) 1586 | end 1587 | if not stored.orbitCamera and camera.ownShare < 0.001 then 1588 | camera:dispose() 1589 | camera = nil 1590 | end 1591 | elseif stored.orbitCamera then 1592 | camera = ac.grabCamera('Paintshop') 1593 | if camera then camera.ownShare = 0 end 1594 | end 1595 | end 1596 | 1597 | local smoothRayDir 1598 | 1599 | local function paintUpdate() 1600 | if activeTool.brush then 1601 | if uiState.isMouseLeftKeyDown then 1602 | if drawing then 1603 | local ray = render.createMouseRay() 1604 | local brush = activeTool.brush 1605 | local tex = activeTool.procBrushTex and activeTool:procBrushTex(ray, false) or brush.brushTex 1606 | editingCanvas:update(function () 1607 | local lastBrushDistance = brushDistance 1608 | local hitDistance = selectedMeshes:raycast(ray) 1609 | if hitDistance ~= -1 then brushDistance = hitDistance end 1610 | if activeTool.stickerMode then 1611 | projectBrushTexture(tex, ray.pos, ray.dir, activeTool:brushColor(), brushDistance) 1612 | if not activeTool.stickerContinious then 1613 | setTimeout(updateAccessibleData) -- projection happens a bit later, so updating data should also be delayed 1614 | drawing = false 1615 | selectedMeshes:setMotionStencil(taaFix.Off) 1616 | ignoreMousePress = true 1617 | end 1618 | return 1619 | elseif lastRay then 1620 | local color = activeTool:brushColor() 1621 | 1622 | if brush.smoothing > 0 then 1623 | smoothRayDir = math.applyLag(smoothRayDir, ray.dir, brush.smoothing ^ 0.3 * 0.9, 0.02) 1624 | ray.dir:set(smoothRayDir) 1625 | end 1626 | 1627 | local distance = ray.pos:clone():addScaled(ray.dir, brushDistance):distance(lastRay.pos:clone():addScaled(lastRay.dir, lastBrushDistance)) 1628 | if distance > brush.brushStepSize then 1629 | local steps = math.min(100, math.floor(0.5 + distance / brush.brushStepSize)) 1630 | for i = 1, steps do 1631 | local p = math.lerp(lastRay.pos, ray.pos, i / steps) 1632 | local d = math.lerp(lastRay.dir, ray.dir, i / steps) 1633 | projectBrushTexture(tex, p, d, color, math.lerp(lastBrushDistance, brushDistance, i / steps)) 1634 | end 1635 | lastRay = ray 1636 | end 1637 | else 1638 | projectBrushTexture(tex, ray.pos, ray.dir, activeTool:brushColor(), brushDistance) 1639 | lastRay = ray 1640 | end 1641 | smoothRayDir = ray.dir:clone() 1642 | end) 1643 | elseif not ignoreMousePress then 1644 | ignoreMousePress = ui.mouseBusy() 1645 | if not ignoreMousePress then 1646 | drawing = true 1647 | selectedMeshes:setMotionStencil(taaFix.On) 1648 | if not uiState.shiftDown then 1649 | lastRay = nil 1650 | end 1651 | setTimeout(function () 1652 | -- adding undo in the next frame, so that dragging mask could cancel drawing operation 1653 | if drawing then 1654 | addUndo(editingCanvas:backup()) 1655 | end 1656 | end) 1657 | end 1658 | end 1659 | else 1660 | if drawing then 1661 | updateAccessibleData() 1662 | selectedMeshes:setMotionStencil(taaFix.Off) 1663 | drawing = false 1664 | end 1665 | ignoreMousePress = false 1666 | end 1667 | elseif activeTool.action then 1668 | activeTool:action() 1669 | end 1670 | 1671 | updateMaskingCanvas() 1672 | updateAOCanvas() 1673 | end 1674 | 1675 | function script.update(dt) 1676 | if not appVisible then 1677 | if camera then 1678 | camera:dispose() 1679 | camera = nil 1680 | end 1681 | return 1682 | end 1683 | if selectedMeshes ~= nil then 1684 | cameraUpdate() 1685 | end 1686 | end 1687 | 1688 | function script.onWorldUpdate(dt) 1689 | if appVisible and selectedMeshes ~= nil then 1690 | ui.setAsynchronousImagesLoading(false) -- when painting, easier to not wait for async images to load 1691 | paintUpdate() 1692 | end 1693 | end 1694 | 1695 | local function rayPlane(ray, opposite) 1696 | local s = opposite and car.look or car.side 1697 | return ray:plane(car.position, s) 1698 | end 1699 | 1700 | local maskingStartMousePos 1701 | 1702 | ---@param ray ray 1703 | local function draggingPoint(index, point, ray) 1704 | local pos = car.bodyTransform:transformPoint(point) 1705 | local hovered = ray:sphere(pos, 0.04) ~= -1 1706 | render.circle(pos, -ac.getCameraForward(), 0.04, 1707 | rgbm(hovered and sim.whiteReferencePoint or 0, sim.whiteReferencePoint, sim.whiteReferencePoint, 0.3), 1708 | rgbm(0, sim.whiteReferencePoint, sim.whiteReferencePoint, 1)) 1709 | if maskingDragging == 0 and uiState.isMouseLeftKeyClicked and hovered then 1710 | maskingStartMousePos = ui.projectPoint(pos) 1711 | maskingDragging = index 1712 | ignoreMousePress = true 1713 | drawing = false 1714 | maskingCreatingFrom = nil 1715 | addUndo(maskingBackup()) 1716 | elseif maskingDragging == index then 1717 | maskingStartMousePos:add(uiState.mouseDelta) 1718 | local r = render.createPointRay(maskingStartMousePos) 1719 | local d = rayPlane(r, index > 2) 1720 | if d ~= -1 then 1721 | point:set(car.worldToLocal:transformPoint(r.pos + r.dir * d)) 1722 | end 1723 | end 1724 | end 1725 | 1726 | function script.draw3D() 1727 | if appVisible and selectedMeshes ~= nil and maskingActive then 1728 | if maskingCreatingFrom ~= nil and maskingCreatingTo ~= nil then 1729 | applyQuickMasking(maskingCreatingFrom, maskingCreatingTo) 1730 | render.circle(car.bodyTransform:transformPoint(maskingPos), car.bodyTransform:transformVector(maskingDir), 3, 1731 | rgbm(sim.whiteReferencePoint, 0, 0, 0.1)) 1732 | return 1733 | end 1734 | 1735 | render.circle(car.bodyTransform:transformPoint(maskingPos), car.bodyTransform:transformVector(maskingDir), 3, 1736 | rgbm(sim.whiteReferencePoint, 0, 0, 0.3)) 1737 | 1738 | local ray = render.createMouseRay() 1739 | if not ui.mouseDown() then maskingDragging = 0 end 1740 | render.setDepthMode(render.DepthMode.Off) 1741 | draggingPoint(1, maskingPoints[1], ray) 1742 | draggingPoint(2, maskingPoints[2], ray) 1743 | draggingPoint(3, maskingPoints[3], ray) 1744 | draggingPoint(4, maskingPoints[4], ray) 1745 | 1746 | if maskingDragging == 1 or maskingDragging == 2 then 1747 | fitMaskingPoints(true) 1748 | elseif maskingDragging == 3 or maskingDragging == 4 then 1749 | fitMaskingPoints(false) 1750 | end 1751 | end 1752 | end 1753 | 1754 | function script.windowMain(dt) 1755 | if brushes == nil then 1756 | rescanBrushes() 1757 | rescanStickers() 1758 | end 1759 | 1760 | ui.pushItemWidth(210) 1761 | ui.setAsynchronousImagesLoading(true) 1762 | if selectedMeshes == nil then 1763 | MeshSelection() 1764 | else 1765 | SkinEditor() 1766 | end 1767 | ui.popItemWidth() 1768 | 1769 | if DebugTex then 1770 | ui.setShadingOffset(1, 0, 1, 1) 1771 | ui.image(DebugTex, 210, rgbm.colors.white, rgbm.colors.red) 1772 | ui.resetShadingOffset() 1773 | end 1774 | end 1775 | 1776 | DebugTex = nil 1777 | 1778 | function script.onShowWindowMain() 1779 | appVisible = true 1780 | end 1781 | 1782 | function script.onHideWindowMain() 1783 | appVisible = false 1784 | if selectedMeshes == nil then 1785 | setTimeout(ac.unloadApp, 1) 1786 | end 1787 | end 1788 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paintshop 2 | 3 | https://github.com/ac-custom-shaders-patch/app-paintshop/assets/3996502/0d2e1786-1222-472a-94f2-1f6553b1ac10 4 | 5 | Paintshop app for drawing on cars in Assetto Corsa. Written in Lua, needs at least CSP 0.1.77 to work. Feel free to use as an example, fork and modify it or anything else. 6 | 7 | # Features 8 | 9 | - Various instruments like brushes, blurring and smudging tools; 10 | - Different tools for mirroring; 11 | - Pen pressure support; 12 | - Hotkeys similar to Photoshop hotkeys; 13 | - Easily extendable with new brushes, stickers, patterns and more. 14 | 15 | # How to install 16 | 17 | - [Download latest release](https://github.com/ac-custom-shaders-patch/app-paintshop/releases/latest/download/Paintshop.zip); 18 | - Drag’n’drop it to Content Manager; 19 | - Or, alternatively, copy apps folder from archive to AC root folder manually. 20 | 21 | # How to use 22 | 23 | - Select a white car skin with no patterns on it; 24 | - Open the app, select car exterior by clicking on it while holding Shift; 25 | - Start drawing; 26 | - Once finished, export the result with button with a printer icon on it; 27 | - Save your original work by using save button. 28 | 29 | The reason for that workflow is because the app would use original texture as an AO map and apply it on top of the result. That’s why save and export are different functions: only export one would apply AO. If you’re loading existing texture with Open button, make sure to select a texture without AO. 30 | 31 | Note: Open and Save buttons have context menus with extra options. Also, brush and stamp selection lists have context menus as well allowing to add new brushes and decals live. 32 | 33 | # Known issues 34 | 35 | - With new skidmarks tools such as projecting other side, mirroring stamp, blur and smudge might not work propely in 0.1.78, this issue will be fixed in 0.1.79. 36 | - Context menus might misbehave and trigger button clicks with Discord fix enabled in general CSP settings. Again, will be fixed in 0.1.79. 37 | 38 | # TODO 39 | 40 | - Layers; 41 | - Layer effects; 42 | - Mask; 43 | - Splines. 44 | -------------------------------------------------------------------------------- /brushes/01 Sharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/01 Sharp.png -------------------------------------------------------------------------------- /brushes/02 Middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/02 Middle.png -------------------------------------------------------------------------------- /brushes/03 Smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/03 Smooth.png -------------------------------------------------------------------------------- /brushes/04 Rough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/04 Rough.png -------------------------------------------------------------------------------- /brushes/05 Uneven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/05 Uneven.png -------------------------------------------------------------------------------- /brushes/06 Chalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/06 Chalk.png -------------------------------------------------------------------------------- /brushes/07 Splatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/07 Splatter.png -------------------------------------------------------------------------------- /brushes/08 Dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/08 Dots.png -------------------------------------------------------------------------------- /brushes/09 Leaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/09 Leaves.png -------------------------------------------------------------------------------- /brushes/10 Triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/brushes/10 Triangle.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Arrow.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Circle.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Ennegon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Ennegon.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Hexagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Hexagon.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Line Narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Line Narrow.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Line.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Octagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Octagon.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Pentagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Pentagon.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Rectangle.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Rhombus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Rhombus.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Semicircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Semicircle.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Square Hollow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Square Hollow.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Square Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Square Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Square Semiround.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Square Semiround.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Square Slightly Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Square Slightly Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Square.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 5 Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 5 Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 5.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 6 Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 6 Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 6.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 8 Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 8 Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Star 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Star 8.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Trapezium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Trapezium.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Triangle Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Triangle Round.png -------------------------------------------------------------------------------- /decals/Basic Shapes/Triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Basic Shapes/Triangle.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Dolphin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Dolphin.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Eagle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Eagle.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Flag.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Flames 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Flames 1.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Flames 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Flames 2.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Flames 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Flames 3.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Lines 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Lines 1.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Lines 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Lines 2.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Mess 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Mess 1.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Puzzle 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Puzzle 1.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Puzzle 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Puzzle 2.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 1.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 2.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 3.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 4.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 5.png -------------------------------------------------------------------------------- /decals/Complex Shapes/Shape 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Complex Shapes/Shape 6.png -------------------------------------------------------------------------------- /decals/Racing/E 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/E 2.png -------------------------------------------------------------------------------- /decals/Racing/Electric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Electric.png -------------------------------------------------------------------------------- /decals/Racing/Esso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Esso.png -------------------------------------------------------------------------------- /decals/Racing/Orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Orange.png -------------------------------------------------------------------------------- /decals/Racing/Tow 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Tow 1.png -------------------------------------------------------------------------------- /decals/Racing/Tow 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Tow 2.png -------------------------------------------------------------------------------- /decals/Racing/Warning 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Warning 1.png -------------------------------------------------------------------------------- /decals/Racing/Warning 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/decals/Racing/Warning 2.png -------------------------------------------------------------------------------- /fonts/ReadMe.txt: -------------------------------------------------------------------------------- 1 | File names should match font names for extra fonts to work. -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/icon.png -------------------------------------------------------------------------------- /manifest.ini: -------------------------------------------------------------------------------- 1 | [ABOUT] 2 | NAME = Paintshop App 3 | AUTHOR = x4fab 4 | VERSION = 1.0 5 | DESCRIPTION = Simple example of a skin drawing app 6 | 7 | [CORE] 8 | LAZY = 1 ; Do not load script until app is first opened. Makes app pretty much zero cost until it’s used, please use it where possible. 9 | ; Using partial laziness here (without automatic unload) so that app could prevent unloading if there is unsaved data. 10 | 11 | [WINDOW_...] 12 | ID = paintshop 13 | NAME = Paintshop 14 | ICON = icon.png 15 | FUNCTION_MAIN = windowMain 16 | FUNCTION_ON_SHOW = onShowWindowMain 17 | FUNCTION_ON_HIDE = onHideWindowMain 18 | MIN_SIZE = 250, 800 19 | MAX_SIZE = 250, 1000 20 | 21 | [SIM_CALLBACKS] 22 | WORLD_UPDATE = onWorldUpdate 23 | 24 | [RENDER_CALLBACKS] 25 | TRANSPARENT = draw3D 26 | -------------------------------------------------------------------------------- /res/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ac-custom-shaders-patch/app-paintshop/aba6dc16883068db6840cf3ec5a7969128190e9c/res/icons.png --------------------------------------------------------------------------------