Compare commits

...

28 commits

Author SHA1 Message Date
6b255ceb59 feat(rendering): stroked text 2025-07-13 16:12:28 +02:00
8f68ae45ce fix(rendering): hint and message should render on the same layer 2025-07-12 20:55:28 +02:00
757e1029b4 feat(rendering): added messages and hints 2025-07-12 20:52:57 +02:00
abe3abaf70 feat(editor): added a proper title 2025-07-12 16:06:22 +02:00
56de603bc7 feat(editor): click on stack trace to go to line in script 2025-07-12 11:20:31 +02:00
d3c1469dd8 feat(lua): track source of scripts to allow hyperlinking to script 2025-07-12 11:07:53 +02:00
ba97c7c574 fix(editor): command bar selects all on press enter 2025-07-12 01:39:06 +02:00
665f8f6151 fix(lua): share environment between command bar calls 2025-07-12 01:35:42 +02:00
58cd6528a3 refactor(lua): generate separate environments for script 2025-07-12 01:30:37 +02:00
b1670f31e8 feat(editor): command bar 2025-07-12 01:12:59 +02:00
c4c767fda9 refactor(logging): changed the way source-linked traces are logged 2025-07-11 10:09:02 +02:00
c58c262ccf refactor(lua): pre-parse pcall wrapper ahead of time 2025-07-11 09:46:50 +02:00
e00aa46911 fix(editor): editing individual component of Vector3 in properties resulted in a crash 2025-07-11 09:35:49 +02:00
cc1da0804e fix(lua): re-added code that ensures that the thread (and therefore any upvalues) don't get gc'ed in signal handler 2025-07-11 01:39:18 +02:00
ebb1b4d3a5 feat(editor): added miscellaneous missing icons for windows 2025-07-11 01:19:06 +02:00
5014c82541 fix(editor): fixed a few dark-theme-related bugs under windows 2025-07-11 00:34:02 +02:00
d258e2b818 feat(lua): added Instance.new for object creation 2025-07-11 00:05:17 +02:00
2135fa957c misc: lua.h => luaapis.h 2025-07-10 23:45:16 +02:00
17cf627cb9 fix(lua): applied previous fix also to signal 2025-07-10 23:37:27 +02:00
14125d755a fix(lua): yet another rewrite of wrapper that supports yielding (via lua) 2025-07-10 23:34:06 +02:00
1e8111c2dd fix(lua): fixed error handling for signals 2025-07-10 20:07:11 +02:00
f28f9f36a2 fix(lua): detect and print syntax errors 2025-07-10 12:16:54 +02:00
2b43098187 fix(editor): standardize line endings to LF for scripts 2025-07-10 12:11:07 +02:00
763bb230a7 fix(cmake): missing freetype dependency + Qt6PrintSupport 2025-07-07 21:31:19 +02:00
1b3b88223d fix(lua): segfault on error handler, new error handler system adopted 2025-07-10 02:08:51 +02:00
e51d00708b refactor(rendering): use VBOs for torus instead of deprecated immediate mode 2025-07-09 15:10:19 +02:00
9c4f2c7495 refactor(rendering): removed glfw dependency of render functions 2025-07-09 15:10:19 +02:00
092c3c748d feat(rendering): ttf font rendering 2025-07-09 15:10:19 +02:00
70 changed files with 1121 additions and 320 deletions

16
assets/font/AUTHORS Normal file
View file

@ -0,0 +1,16 @@
AUTHORS
Current Contributors (sorted alphabetically):
- Vishal Vijayraghavan <vishalvvr at fedoraproject dot org>
Project Owner/ Maintainer (Current)
Red Hat, Inc.
Previous Contributors
- Pravin Satpute <psatpute at redhat dot com>
Project Owner/ Maintainer
Red Hat, Inc.
- Steve Matteson
Original Designer
Ascender, Inc.

BIN
assets/font/Arial.TTF Normal file

Binary file not shown.

102
assets/font/LICENSE Normal file
View file

@ -0,0 +1,102 @@
Digitized data copyright (c) 2010 Google Corporation
with Reserved Font Arimo, Tinos and Cousine.
Copyright (c) 2012 Red Hat, Inc.
with Reserved Font Name Liberation.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
worldwide development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to provide
a free and open framework in which fonts may be shared and improved in
partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves.
The fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such.
This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components
as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting ? in part or in whole ?
any of the components of the Original Version, by changing formats or
by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer
or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this license, and must not be distributed
under any other license. The requirement for fonts to remain under
this license does not apply to any document created using the Font
Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
Icons transform-move, transform-scale, and transform-rotate were modified from silk icons by maelstrom

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

BIN
assets/icons/message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View file

@ -0,0 +1,22 @@
#version 330 core
out vec4 FragColor;
in vec3 vPos;
in vec2 vTexCoord;
uniform sampler2D fontTex;
uniform int charIndex;
// Main
void main() {
int x = (charIndex-32) % 16;
int y = (charIndex-32) / 16;
float fx = float(x) / 16;
float fy = float(y) / 8;
vec4 color = texture(fontTex, vec2(fx, fy) + vTexCoord * vec2(1.f/32, 1.f/16));
FragColor = vec3(color) == vec3(0, 0, 0) ? vec4(0, 0, 0, 0) : color;
// FragColor = color;
}

View file

@ -0,0 +1,12 @@
#version 330 core
in vec3 aPos;
in vec2 aTexCoord;
out vec3 vPos;
out vec2 vTexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
vPos = aPos;
vTexCoord = aTexCoord;
}

View file

@ -1,22 +1,14 @@
// https://learnopengl.com/In-Practice/Text-Rendering
#version 330 core
in vec2 TexCoords;
out vec4 color;
out vec4 FragColor;
in vec3 vPos;
in vec2 vTexCoord;
uniform sampler2D text;
uniform vec3 textColor;
uniform sampler2D fontTex;
uniform int charIndex;
// Main
void main() {
int x = (charIndex-32) % 16;
int y = (charIndex-32) / 16;
float fx = float(x) / 16;
float fy = float(y) / 8;
vec4 color = texture(fontTex, vec2(fx, fy) + vTexCoord * vec2(1.f/32, 1.f/16));
FragColor = vec3(color) == vec3(0, 0, 0) ? vec4(0, 0, 0, 0) : color;
// FragColor = color;
}
void main()
{
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled;
}

View file

@ -1,12 +1,13 @@
#version 330 core
in vec3 aPos;
in vec2 aTexCoord;
// https://learnopengl.com/In-Practice/Text-Rendering
#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
out vec2 TexCoords;
uniform mat4 projection;
out vec3 vPos;
out vec2 vTexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
vPos = aPos;
vTexCoord = aTexCoord;
}
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
TexCoords = vertex.zw;
}

View file

@ -3,10 +3,10 @@
// I/O
out vec4 fColor;
uniform vec3 aColor;
uniform vec4 aColor;
// Main
void main() {
fColor = vec4(aColor, 1.0);
fColor = aColor;
}

View file

@ -34,7 +34,7 @@ int main() {
glewInit();
gDataModel->Init();
renderInit(window, 1200, 900);
renderInit(1200, 900);
// Baseplate
gWorkspace()->AddChild(Part::New({
@ -66,7 +66,7 @@ int main() {
processInput(window);
gWorkspace()->PhysicsStep(deltaTime);
render(window);
render();
glfwSwapBuffers(window);
glfwPollEvents();
@ -77,7 +77,7 @@ int main() {
}
void errorCatcher(int id, const char* str) {
Logger::fatalError(std::format("GLFW Error: [{}] {}", id, str));
Logger::fatalErrorf("GLFW Error: [{}] {}", id, str);
}
float lastTime;

View file

@ -7,6 +7,7 @@ find_package(OpenGL)
find_package(glm CONFIG REQUIRED)
find_package(ReactPhysics3D REQUIRED)
find_package(pugixml 1.15 REQUIRED)
find_package(Freetype)
find_package(Stb REQUIRED)
include_directories(${Stb_INCLUDE_DIR})
@ -44,7 +45,7 @@ list(APPEND SOURCES ${AUTOGEN_OUTS})
add_library(openblocks STATIC ${SOURCES})
set_target_properties(openblocks PROPERTIES OUTPUT_NAME "openblocks")
target_link_directories(openblocks PUBLIC ${LUAJIT_LIBRARY_DIRS})
target_link_libraries(openblocks ${GLEW_LIBRARIES} ${LUAJIT_LIBRARIES} OpenGL::GL ReactPhysics3D::ReactPhysics3D pugixml::pugixml)
target_link_libraries(openblocks ${GLEW_LIBRARIES} ${LUAJIT_LIBRARIES} OpenGL::GL ReactPhysics3D::ReactPhysics3D pugixml::pugixml Freetype::Freetype)
target_include_directories(openblocks PUBLIC "src" "../include" ${LUAJIT_INCLUDE_DIRS})
add_dependencies(openblocks autogen_build autogen)

View file

@ -6,7 +6,7 @@
#include <vector>
#include "datatypes/annotation.h"
#include "error/data.h"
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
struct _EnumData {
std::string name;

View file

@ -2,7 +2,7 @@
#include "error/data.h"
#include "variant.h"
#include <pugixml.hpp>
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
#include <sstream>
// null

View file

@ -6,7 +6,7 @@
#include <memory>
#include <optional>
#include "objects/base/instance.h"
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
#include "objects/base/member.h"
#include <pugixml.hpp>

View file

@ -1,12 +1,14 @@
#include "signal.h"
#include "datatypes/base.h"
#include "variant.h"
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
#include <cstdio>
#include <pugixml.hpp>
#include <memory>
#include <vector>
int script_errhandler(lua_State*); // extern
SignalSource::SignalSource() : std::shared_ptr<Signal>(std::make_shared<Signal>()) {}
SignalSource::~SignalSource() = default;
@ -25,6 +27,8 @@ LuaSignalConnection::LuaSignalConnection(lua_State* L, std::weak_ptr<Signal> par
// Save function and current thread so they don't get GC'd
function = luaL_ref(L, LUA_REGISTRYINDEX);
lua_pushthread(L);
// For posterity, since I accidentally removed this once, the parent thread must not be
// deleted because it may (likely) contain upvalues that the handler references
thread = luaL_ref(L, LUA_REGISTRYINDEX);
}
@ -34,40 +38,24 @@ LuaSignalConnection::~LuaSignalConnection() {
luaL_unref(state, LUA_REGISTRYINDEX, thread);
}
#if 0
static void stackdump(lua_State* L) {
printf("%d\n", lua_gettop(L));
fflush(stdout);
lua_getfield(L, LUA_GLOBALSINDEX, "tostring");
for (int i = lua_gettop(L)-1; i >= 1; i--) {
lua_pushvalue(L, -1);
lua_pushvalue(L, i);
lua_call(L, 1, 1);
const char* str = lua_tostring(L, -1);
lua_pop(L, 1);
printf("%s: %s\n", lua_typename(L, lua_type(L, i)), str);
}
lua_pop(L, 1);
printf("\n\n");
fflush(stdout);
}
#endif
void LuaSignalConnection::Call(std::vector<Variant> args) {
lua_State* thread = lua_newthread(state);
// Push function
// Push wrapepr as thread function
lua_getfield(thread, LUA_REGISTRYINDEX, "LuaPCallWrapper");
// Push function as upvalue for wrapper
lua_rawgeti(thread, LUA_REGISTRYINDEX, function);
// Also push our error handler and generate wrapped function
lua_pushcfunction(thread, script_errhandler);
lua_call(thread, 2, 1);
for (Variant arg : args) {
arg.PushLuaValue(thread);
}
int status = lua_resume(thread, args.size());
if (status > LUA_YIELD) {
Logger::error(lua_tostring(thread, -1));
lua_pop(thread, 1); // Pop return value
}
lua_resume(thread, args.size());
lua_pop(state, 1); // Pop thread
}
@ -152,11 +140,7 @@ void Signal::Fire(std::vector<Variant> args) {
arg.PushLuaValue(thread);
}
int status = lua_resume(thread, args.size());
if (status > LUA_YIELD) {
Logger::error(lua_tostring(thread, -1));
lua_pop(thread, 1); // Pop return value
}
lua_resume(thread, args.size());
// Remove thread from registry
luaL_unref(thread, LUA_REGISTRYINDEX, threadId);

View file

@ -9,7 +9,6 @@
static std::ofstream logStream;
static std::vector<Logger::LogListener> logListeners;
static std::vector<Logger::TraceLogListener> traceLogListeners;
std::string Logger::currentLogDir = "NULL";
void Logger::init() {
@ -28,7 +27,7 @@ void Logger::finish() {
logStream.close();
}
void Logger::log(std::string message, Logger::LogLevel logLevel) {
void Logger::log(std::string message, Logger::LogLevel logLevel, ScriptSource source) {
std::string logLevelStr = logLevel == Logger::LogLevel::INFO ? "INFO" :
logLevel == Logger::LogLevel::DEBUG ? "DEBUG" :
logLevel == Logger::LogLevel::TRACE ? "TRACE" :
@ -44,7 +43,7 @@ void Logger::log(std::string message, Logger::LogLevel logLevel) {
printf("%s\n", formattedLogLine.c_str());
for (Logger::LogListener listener : logListeners) {
listener(logLevel, message);
listener(logLevel, message, source);
}
if (logLevel == Logger::LogLevel::FATAL_ERROR) {
@ -52,20 +51,6 @@ void Logger::log(std::string message, Logger::LogLevel logLevel) {
}
}
void Logger::trace(std::string source, int line, void* userData) {
std::string message = "'" + source + "' Line " + std::to_string(line);
log(message, Logger::LogLevel::TRACE);
for (Logger::TraceLogListener listener : traceLogListeners) {
listener(message, source, line, userData);
}
}
void Logger::addLogListener(Logger::LogListener listener) {
logListeners.push_back(listener);
}
void Logger::addLogListener(Logger::TraceLogListener listener) {
traceLogListeners.push_back(listener);
}

View file

@ -1,9 +1,11 @@
#pragma once
#include <format>
#include <functional>
#include <memory>
#include <string>
class Script;
namespace Logger {
enum class LogLevel {
INFO,
@ -14,34 +16,39 @@ namespace Logger {
FATAL_ERROR,
};
typedef std::function<void(LogLevel logLevel, std::string message)> LogListener;
typedef std::function<void(std::string message, std::string source, int line, void* userData)> TraceLogListener;
struct ScriptSource {
std::shared_ptr<Script> script;
int line;
};
typedef std::function<void(LogLevel logLevel, std::string message, ScriptSource source)> LogListener;
extern std::string currentLogDir;
void init();
void finish();
void addLogListener(LogListener);
void addLogListener(TraceLogListener);
void log(std::string message, LogLevel logLevel);
void log(std::string message, LogLevel logLevel, ScriptSource source = {});
inline void info(std::string message) { log(message, LogLevel::INFO); }
inline void debug(std::string message) { log(message, LogLevel::DEBUG); }
inline void warning(std::string message) { log(message, LogLevel::WARNING); }
inline void error(std::string message) { log(message, LogLevel::ERROR); }
inline void fatalError(std::string message) { log(message, LogLevel::FATAL_ERROR); }
inline void trace(std::string message) { log(message, LogLevel::TRACE); };
inline void traceStart() { log("Stack start", LogLevel::TRACE); }
inline void traceEnd() { log("Stack end", LogLevel::TRACE); }
void trace(std::string source, int line, void* userData = nullptr);
template <typename ...Args>
void scriptLogf(std::string format, LogLevel logLevel, ScriptSource source, Args&&... args) {
char message[200];
sprintf(message, format.c_str(), args...);
log(message, logLevel, source);
}
template <typename ...Args>
void logf(std::string format, LogLevel logLevel, Args&&... args) {
char message[200];
sprintf(message, format.c_str(), args...);
log(message, logLevel);
scriptLogf(format, logLevel, {}, args...);
}
template <typename ...Args> inline void infof(std::string format, Args&&... args) { logf(format, LogLevel::INFO, args...); }
template <typename ...Args> inline void debugf(std::string format, Args&&... args) { logf(format, LogLevel::DEBUG, args...); }
template <typename ...Args> inline void warningf(std::string format, Args&&... args) { logf(format, LogLevel::WARNING, args...); }

View file

@ -0,0 +1,77 @@
#include "objects/base/instance.h"
#include "datatypes/ref.h"
#include "luaapis.h" // IWYU pragma: keep
#include "objects/meta.h"
#include <memory>
static int lib_Instance_index(lua_State*);
static int lib_Instance_tostring(lua_State*);
static const struct luaL_Reg lib_Instance_metatable [] = {
{"__index", lib_Instance_index},
{"__tostring", lib_Instance_tostring},
{NULL, NULL} /* end of array */
};
static int __lua_impl__Instance__new(lua_State* L) {
int n = lua_gettop(L);
// First argument (className)
std::string className = luaL_checkstring(L, 1);
// [Optional] Second argument (parent)
std::shared_ptr<Instance> parent;
if (n > 1) {
parent = **(std::shared_ptr<Instance>**)luaL_checkudata(L, 2, "__mt_instance");
}
// Look up class name
if (INSTANCE_MAP.count(className) == 0)
return luaL_error(L, "Attempt to create instance of unknown type '%s'", className.c_str());
const InstanceType* type = INSTANCE_MAP[className];
if (type->flags & (INSTANCE_NOTCREATABLE | INSTANCE_SERVICE) || type->constructor == nullptr)
return luaL_error(L, "Attempt to create Instance of type '%s', which is not creatable", className.c_str());
std::shared_ptr<Instance> object = type->constructor();
if (parent != nullptr)
object->SetParent(parent);
InstanceRef(object).PushLuaValue(L);
return 1;
}
void Instance::PushLuaLibrary(lua_State* L) {
lua_getglobal(L, "_G");
lua_pushstring(L, "Instance");
lua_newuserdata(L, 0);
// Create the library's metatable
luaL_newmetatable(L, "__mt_lib_Instance");
luaL_register(L, NULL, lib_Instance_metatable);
lua_setmetatable(L, -2);
lua_rawset(L, -3);
lua_pop(L, 1);
}
int lib_Instance_tostring(lua_State* L) {
lua_pushstring(L, "Instance");
return 1;
}
static int lib_Instance_index(lua_State* L) {
std::string key(lua_tostring(L, 2));
lua_pop(L, 2);
if (key == "new") {
lua_pushcfunction(L, __lua_impl__Instance__new);
return 1;
}
return luaL_error(L, "%s is not a valid member of %s\n", key.c_str(), "Instance");
}

View file

@ -95,6 +95,7 @@ public:
// Instance is abstract, so it should not implement GetClass directly
virtual const InstanceType* GetClass() = 0;
static void PushLuaLibrary(lua_State*); // Defined in lua/instancelib.cpp
bool SetParent(std::optional<std::shared_ptr<Instance>> newParent);
std::optional<std::shared_ptr<Instance>> GetParent();
bool IsParentLocked();

View file

@ -4,7 +4,7 @@
#include "panic.h"
#include <memory>
Service::Service(const InstanceType* type) : Instance(type){}
Service::Service(const InstanceType* type) : Instance(type) {}
// Fail if parented to non-datamodel, otherwise lock parent
void Service::OnParentUpdated(std::optional<std::shared_ptr<Instance>> oldParent, std::optional<std::shared_ptr<Instance>> newParent) {

View file

@ -0,0 +1,4 @@
#include "hint.h"
Hint::Hint(): Message(&TYPE) {}
Hint::~Hint() = default;

18
core/src/objects/hint.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include "objects/annotation.h"
#include "objects/base/instance.h"
#include "objects/message.h"
#include <memory>
// Dims the player's screen and displays some centered text
class DEF_INST_(explorer_icon="message") Hint : public Message {
AUTOGEN_PREAMBLE
public:
Hint();
~Hint();
static inline std::shared_ptr<Hint> New() { return std::make_shared<Hint>(); };
static inline std::shared_ptr<Instance> Create() { return std::make_shared<Hint>(); };
};

View file

@ -0,0 +1,5 @@
#include "message.h"
Message::Message(const InstanceType* type) : Instance(type) {}
Message::Message(): Instance(&TYPE) {}
Message::~Message() = default;

View file

@ -0,0 +1,21 @@
#pragma once
#include "objects/annotation.h"
#include "objects/base/instance.h"
#include <memory>
// Dims the player's screen and displays some centered text
class DEF_INST_(explorer_icon="message") Message : public Instance {
AUTOGEN_PREAMBLE
protected:
Message(const InstanceType* type);
public:
Message();
~Message();
DEF_PROP std::string text;
static inline std::shared_ptr<Message> New() { return std::make_shared<Message>(); };
static inline std::shared_ptr<Instance> Create() { return std::make_shared<Message>(); };
};

View file

@ -1,9 +1,11 @@
#include "meta.h"
#include "objects/folder.h"
#include "objects/hint.h"
#include "objects/joint/jointinstance.h"
#include "objects/joint/rotate.h"
#include "objects/joint/rotatev.h"
#include "objects/joint/weld.h"
#include "objects/message.h"
#include "objects/service/jointsservice.h"
#include "objects/model.h"
#include "objects/part.h"
@ -27,6 +29,8 @@ std::map<std::string, const InstanceType*> INSTANCE_MAP = {
{ "JointInstance", &JointInstance::TYPE },
{ "Script", &Script::TYPE },
{ "Model", &Model::TYPE },
{ "Message", &Message::TYPE },
{ "Hint", &Hint::TYPE },
// { "Folder", &Folder::TYPE },
// Services

View file

@ -1,5 +1,7 @@
#include "script.h"
#include "common.h"
#include "datatypes/variant.h"
#include "lauxlib.h"
#include "logger.h"
#include "objects/base/instance.h"
#include "objects/base/member.h"
@ -7,12 +9,11 @@
#include "objects/service/workspace.h"
#include "objects/datamodel.h"
#include "datatypes/ref.h"
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
#include <algorithm>
#include <memory>
int script_wait(lua_State*);
int script_delay(lua_State*);
int script_errhandler(lua_State*);
Script::Script(): Instance(&TYPE) {
source = "print(\"Hello, world!\")";
@ -25,101 +26,106 @@ void Script::Run() {
std::shared_ptr<ScriptContext> scriptContext = dataModel().value()->GetService<ScriptContext>();
lua_State* L = scriptContext->state;
int top = lua_gettop(L);
// Create thread
this->thread = lua_newthread(L);
lua_State* Lt = thread;
// Initialize script globals
lua_getglobal(Lt, "_G");
// Push wrapper as thread function
lua_getfield(Lt, LUA_REGISTRYINDEX, "LuaPCallWrapper");
InstanceRef(shared_from_this()).PushLuaValue(Lt);
lua_setfield(Lt, -2, "script");
InstanceRef(dataModel().value()).PushLuaValue(Lt);
lua_setfield(Lt, -2, "game");
// Load source code and push onto thread as upvalue for wrapper
int status = luaL_loadbuffer(Lt, source.c_str(), source.size(), this->GetFullName().c_str());
if (status != LUA_OK) {
// Failed to parse/load chunk
Logger::error(lua_tostring(Lt, -1));
InstanceRef(dataModel().value()->GetService<Workspace>()).PushLuaValue(Lt);
lua_setfield(Lt, -2, "workspace");
lua_pushlightuserdata(Lt, scriptContext.get());
lua_pushcclosure(Lt, script_wait, 1);
lua_setfield(Lt, -2, "wait");
lua_pushlightuserdata(Lt, scriptContext.get());
lua_pushcclosure(Lt, script_delay, 1);
lua_setfield(Lt, -2, "delay");
lua_pop(Lt, 1); // _G
// Load source and push onto thread stack as function ptr
// luaL_loadstring(Lt, source.c_str());
luaL_loadbuffer(Lt, source.c_str(), source.size(), scriptContext->RegisterScriptSource(shared<Script>()).c_str());
int status = lua_resume(Lt, 0);
if (status > LUA_YIELD) {
lua_Debug dbg;
lua_getstack(Lt, 1, &dbg);
lua_getinfo(Lt, "S", &dbg);
std::weak_ptr<Script> source = scriptContext->GetScriptFromSource(dbg.source);
std::string errorMessage = lua_tostring(Lt, -1);
if (!source.expired())
errorMessage = source.lock()->GetFullName() + errorMessage.substr(errorMessage.find(':'));
Logger::error(errorMessage);
lua_pop(Lt, 1); // Pop return value
Logger::traceStart();
int stack = 1;
while (lua_getstack(Lt, stack++, &dbg)) {
lua_getinfo(Lt, "nlSu", &dbg);
std::weak_ptr<Script> source = scriptContext->GetScriptFromSource(dbg.source);
if (source.expired()) {
Logger::trace(dbg.source, dbg.currentline);
} else {
Logger::trace(source.lock()->GetFullName(), dbg.currentline, &source);
}
}
Logger::traceEnd();
lua_settop(L, top);
return;
}
// Initialize script globals
scriptContext->NewEnvironment(Lt); // Pushes envtable, metatable
// Set script in metatable source
InstanceRef(shared_from_this()).PushLuaValue(Lt);
lua_setfield(Lt, -2, "source");
lua_pop(Lt, 1); // Pop metatable
// Set script in environment
InstanceRef(shared_from_this()).PushLuaValue(Lt);
lua_setfield(Lt, -2, "script");
lua_setfenv(Lt, -2); // Set env of loaded function
// Push our error handler and then generate the wrapped function
lua_pushcfunction(Lt, script_errhandler);
lua_call(Lt, 2, 1);
// Resume the thread
lua_resume(Lt, 0);
lua_pop(L, 1); // Pop the thread
lua_settop(L, top);
}
void Script::Stop() {
// TODO:
}
int script_wait(lua_State* L) {
ScriptContext* scriptContext = (ScriptContext*)lua_touserdata(L, lua_upvalueindex(1));
float secs = lua_gettop(L) == 0 ? 0.03 : std::max(luaL_checknumber(L, 1), 0.03);
if (lua_gettop(L) > 0) lua_pop(L, 1); // pop secs
static std::shared_ptr<Script> getfsource(lua_State* L, lua_Debug* dbg) {
int top = lua_gettop(L);
scriptContext->PushThreadSleep(L, secs);
lua_getinfo(L, "f", dbg);
lua_getfenv(L, -1); // Get fenv of stack pos
if (lua_isnil(L, -1)) { // No env could be found
lua_settop(L, top);
return nullptr;
}
// Yield
return lua_yield(L, 0);
// Get source from metatable
lua_getmetatable(L, -1);
lua_getfield(L, -1, "source");
auto result = InstanceRef::FromLuaValue(L, -1);
if (!result) {
lua_settop(L, top);
return nullptr;
}
lua_settop(L, top);
std::shared_ptr<Instance> ref = result.expect().get<InstanceRef>();
if (!ref->IsA<Script>()) return nullptr;
return ref->CastTo<Script>().expect();
}
int script_delay(lua_State* L) {
ScriptContext* scriptContext = (ScriptContext*)lua_touserdata(L, lua_upvalueindex(1));
float secs = std::max(luaL_checknumber(L, 1), 0.03);
luaL_checktype(L, 2, LUA_TFUNCTION);
int script_errhandler(lua_State* L) {
std::string errorMessage = lua_tostring(L, -1);
Logger::error(errorMessage);
lua_State* Lt = lua_newthread(L); // Create a new thread
// I think this is memory abuse??
// Wouldn't popping the thread in this case make it eligible for garbage collection?
lua_pop(L, 1); // pop the newly created thread so that xmove moves func instead of it into itself
lua_xmove(L, Lt, 1); // move func
lua_pop(L, 1); // pop secs
// Traceback
// Schedule next run
scriptContext->PushThreadSleep(Lt, secs);
Logger::trace("Stack start");
lua_Debug dbg;
int stack = 1;
while (lua_getstack(L, stack++, &dbg)) {
lua_getinfo(L, "nlSu", &dbg);
// Ignore C frames and internal wrappers
if (strcmp(dbg.what, "C") == 0 || strcmp(dbg.source, "=PCALL_WRAPPER") == 0)
continue;
// Find script source
std::shared_ptr<Script> source = getfsource(L, &dbg);
Logger::scriptLogf("'%s', Line %d", Logger::LogLevel::TRACE, {source, dbg.currentline}, dbg.source, dbg.currentline);
}
Logger::trace("Stack end");
return 0;
}

View file

@ -1,22 +1,30 @@
#include "scriptcontext.h"
#include "datatypes/cframe.h"
#include "datatypes/color3.h"
#include "datatypes/ref.h"
#include "datatypes/vector.h"
#include "logger.h"
#include "objects/datamodel.h"
#include "objects/service/workspace.h"
#include "timeutil.h"
#include <ctime>
#include <string>
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
const char* WRAPPER_SRC = "local func, errhandler = ... return function(...) local args = {...} xpcall(function() func(unpack(args)) end, errhandler) end";
int g_wait(lua_State*);
int g_delay(lua_State*);
static int g_print(lua_State*);
static int g_require(lua_State*);
static const struct luaL_Reg luaglobals [] = {
static const luaL_Reg luaglobals [] = {
{"print", g_print},
{"require", g_require},
{NULL, NULL} /* end of array */
};
std::string unsafe_globals[] = {
// Todo implement our own "safe" setfenv/getfenv
"loadfile", "loadstring", "load", "dofile", "getfenv", "setfenv"
};
@ -46,6 +54,30 @@ void ScriptContext::InitService() {
Vector3::PushLuaLibrary(state);
CFrame::PushLuaLibrary(state);
Color3::PushLuaLibrary(state);
Instance::PushLuaLibrary(state);
// Add other globals
lua_getglobal(state, "_G");
InstanceRef(dataModel().value()).PushLuaValue(state);
lua_setfield(state, -2, "game");
InstanceRef(dataModel().value()->GetService<Workspace>()).PushLuaValue(state);
lua_setfield(state, -2, "workspace");
lua_pushlightuserdata(state, this);
lua_pushcclosure(state, g_wait, 1);
lua_setfield(state, -2, "wait");
lua_pushlightuserdata(state, this);
lua_pushcclosure(state, g_delay, 1);
lua_setfield(state, -2, "delay");
lua_pop(state, 1); // _G
// Add wrapper function
luaL_loadbuffer(state, WRAPPER_SRC, strlen(WRAPPER_SRC), "=PCALL_WRAPPER");
lua_setfield(state, LUA_REGISTRYINDEX, "LuaPCallWrapper");
// TODO: custom os library
@ -105,11 +137,7 @@ void ScriptContext::RunSleepingThreads() {
// Time args
lua_pushnumber(sleep.thread, float(tu_clock_micros() - sleep.timeYieldedWhen) / 1'000'000);
lua_pushnumber(sleep.thread, float(tu_clock_micros()) / 1'000'000);
int status = lua_resume(sleep.thread, 2);
if (status > LUA_YIELD) {
Logger::error(lua_tostring(sleep.thread, -1));
lua_pop(sleep.thread, 1); // Pop return value
}
lua_resume(sleep.thread, 2);
// Remove thread
deleted = true;
@ -134,21 +162,25 @@ void ScriptContext::RunSleepingThreads() {
schedTime = tu_clock_micros() - startTime;
}
std::string ScriptContext::RegisterScriptSource(std::shared_ptr<Script> script) {
// If it has already been registered, reference it here
for (auto& [id, script2] : scriptSources) {
if (!script2.expired() && script2.lock() == script)
return "=f" + std::to_string(id);
}
void ScriptContext::NewEnvironment(lua_State* L) {
lua_newtable(L); // Env table
lua_newtable(L); // Metatable
int id = lastScriptSourceId++;
scriptSources[id] = script;
return "=f" + std::to_string(id);
}
// Push __index
lua_pushvalue(L, LUA_GLOBALSINDEX);
lua_setfield(L, -2, "__index");
std::weak_ptr<Script> ScriptContext::GetScriptFromSource(std::string source) {
int id = std::stoi(source.c_str() + 2);
return scriptSources[id];
// Push __metatable
lua_pushstring(L, "metatable is locked");
lua_setfield(L, -2, "__metatable");
// Copy metatable and set the env table's metatable
lua_pushvalue(L, -1);
lua_setmetatable(L, -3);
// Remainder on stack:
// 1. Env table
// 2. Metatable
}
// https://www.lua.org/source/5.1/lbaselib.c.html
@ -181,4 +213,33 @@ static int g_require(lua_State* L) {
if (nargs < 1) return luaL_error(L, "expected argument module");
return luaL_error(L, "require is not yet implemented");
}
int g_wait(lua_State* L) {
ScriptContext* scriptContext = (ScriptContext*)lua_touserdata(L, lua_upvalueindex(1));
float secs = lua_gettop(L) == 0 ? 0.03 : std::max(luaL_checknumber(L, 1), 0.03);
if (lua_gettop(L) > 0) lua_pop(L, 1); // pop secs
scriptContext->PushThreadSleep(L, secs);
// Yield
return lua_yield(L, 0);
}
int g_delay(lua_State* L) {
ScriptContext* scriptContext = (ScriptContext*)lua_touserdata(L, lua_upvalueindex(1));
float secs = std::max(luaL_checknumber(L, 1), 0.03);
luaL_checktype(L, 2, LUA_TFUNCTION);
lua_State* Lt = lua_newthread(L); // Create a new thread
// I think this is memory abuse??
// Wouldn't popping the thread in this case make it eligible for garbage collection?
lua_pop(L, 1); // pop the newly created thread so that xmove moves func instead of it into itself
lua_xmove(L, Lt, 1); // move func
lua_pop(L, 1); // pop secs
// Schedule next run
scriptContext->PushThreadSleep(Lt, secs);
return 0;
}

View file

@ -2,7 +2,7 @@
#include "objects/annotation.h"
#include "objects/base/service.h"
#include "lua.h" // IWYU pragma: keep
#include "luaapis.h" // IWYU pragma: keep
#include <memory>
#include <vector>
@ -19,7 +19,6 @@ class DEF_INST_SERVICE ScriptContext : public Service {
AUTOGEN_PREAMBLE
std::vector<SleepingThread> sleepingThreads;
std::map<int, std::weak_ptr<Script>> scriptSources;
int lastScriptSourceId = 0;
protected:
void InitService() override;
@ -32,8 +31,9 @@ public:
lua_State* state;
void PushThreadSleep(lua_State* thread, float delay);
void RunSleepingThreads();
std::string RegisterScriptSource(std::shared_ptr<Script>);
std::weak_ptr<Script> GetScriptFromSource(std::string source);
// Generates an environment with a metatable and pushes it both the env table and metatable in order onto the stack
void NewEnvironment(lua_State* state);
static inline std::shared_ptr<Instance> Create() { return std::make_shared<ScriptContext>(); };
};

View file

@ -7,19 +7,21 @@
#include <string>
extern int viewportWidth, viewportHeight;
extern Texture* fontTexture;
extern Shader* fontShader;
extern Texture* debugFontTexture;
extern Shader* debugFontShader;
extern Shader* identityShader;
void drawChar(char c, int x, int y, float scale=1.f) {
fontShader->use();
fontTexture->activate(1);
fontShader->set("fontTex", 1);
void drawRect(int x, int y, int width, int height, glm::vec4 color);
fontShader->set("charIndex", (int)c);
void drawChar(char c, int x, int y, float scale=1.f) {
debugFontShader->use();
debugFontTexture->activate(1);
debugFontShader->set("fontTex", 1);
debugFontShader->set("charIndex", (int)c);
// https://stackoverflow.com/a/10631263
int tex = fontShader->getAttribute("aTexCoord");
int tex = debugFontShader->getAttribute("aTexCoord");
y = viewportHeight - y - 16*scale;
float x0 = float(x)/viewportWidth, y0 = float(y)/viewportHeight, x1 = ((float)x + 8*scale)/viewportWidth, y1 = ((float)y + 16*scale)/viewportHeight;
@ -41,22 +43,6 @@ void drawString(std::string str, int x, int y, float scale=1.f) {
}
}
void drawRect(int x, int y, int w, int h, glm::vec4 color) {
identityShader->use();
identityShader->set("aColor", color);
float x0 = 2*float(x)/viewportWidth-1, y0 = 2*float(y)/viewportHeight-1, x1 = 2*float(x + w)/viewportWidth-1, y1 = 2*float(y + h)/viewportHeight-1;
float tmp;
tmp = -y0, y0 = -y1, y1 = tmp;
glBegin(GL_QUADS);
glVertex3f(x0, y0, 0);
glVertex3f(x1, y0, 0);
glVertex3f(x1, y1, 0);
glVertex3f(x0, y1, 0);
glEnd();
}
static tu_time_t lastTime;
extern tu_time_t renderTime;
extern tu_time_t physTime;

208
core/src/rendering/font.cpp Normal file
View file

@ -0,0 +1,208 @@
#include "font.h"
#include "logger.h"
#include "panic.h"
#include "rendering/shader.h"
#include <GL/glew.h>
#include <GL/gl.h>
#include <glm/ext/matrix_clip_space.hpp>
#include <memory>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_STROKER_H
// https://learnopengl.com/In-Practice/Text-Rendering
FT_Library freetype;
Shader* fontShader;
extern int viewportWidth, viewportHeight;
unsigned int textVAO, textVBO;
void fontInit() {
if (FT_Error err = FT_Init_FreeType(&freetype)) {
Logger::fatalErrorf("Failed to initialize Freetype: [%d]", err);
panic();
}
fontShader = new Shader("assets/shaders/font.vs", "assets/shaders/font.fs");
// Set up buffer
glGenVertexArrays(1, &textVAO);
glBindVertexArray(textVAO);
glGenBuffers(1, &textVBO);
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
// Dynamic, because we update the vertices often V~~~~~~~~~~~~~~
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void fontFinish() {
if (FT_Error err = FT_Done_FreeType(freetype)) {
Logger::fatalErrorf("Failed to free Freetype: [%d]", err);
panic();
}
}
static void loadCharTexture(FT_Face& face, FT_BitmapGlyph& glyph_bitmap, Character& character) {
// Generate texture
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RED,
glyph_bitmap->bitmap.width,
glyph_bitmap->bitmap.rows,
0,
GL_RED,
GL_UNSIGNED_BYTE,
glyph_bitmap->bitmap.buffer
);
// set texture options
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// now store character for later use
character.ID = texture;
character.size = glm::ivec2(glyph_bitmap->bitmap.width, glyph_bitmap->bitmap.rows);
character.bearing = glm::ivec2(glyph_bitmap->left, glyph_bitmap->top);
character.advance = (unsigned int)face->glyph->advance.x;
}
std::shared_ptr<Font> loadFont(std::string fontName) {
std::string fontPath = "assets/font/" + fontName;
FT_Face face;
if (FT_Error err = FT_New_Face(freetype, fontPath.c_str(), 0, &face)) {
Logger::fatalErrorf("Failed to create font '%s': [%d]", fontName.c_str(), err);
panic();
}
std::shared_ptr<Font> font = std::make_shared<Font>();
FT_Set_Pixel_Sizes(face, 0, 16);
font->height = face->size->metrics.y_ppem;
FT_Stroker stroker;
FT_Stroker_New(freetype, &stroker);
FT_Stroker_Set(stroker, 2 * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
// Load each glyph
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (unsigned char c = 0; c < 128; c++) {
// load character glyph
if (FT_Error err = FT_Load_Char(face, c, FT_LOAD_DEFAULT)) {
Logger::errorf("Failed to load glyph %d in font '%s': [%d]", c, fontName.c_str(), err);
continue;
}
FT_Glyph glyph;
FT_BitmapGlyph glyph_bitmap;
Character character;
// Render base
FT_Get_Glyph(face->glyph, &glyph);
FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, nullptr, true);
glyph_bitmap = (FT_BitmapGlyph)glyph;
loadCharTexture(face, glyph_bitmap, character);
font->characters[c] = character;
FT_Done_Glyph(glyph);
// TODO: Find out how to clear FT_BitmapGlyph... I cant import FT_Bitmap_Done for some reason
// Render stroked
// https://stackoverflow.com/a/28078293/16255372
FT_Get_Glyph(face->glyph, &glyph);
FT_Glyph_StrokeBorder(&glyph, stroker, false, true);
FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, nullptr, true);
glyph_bitmap = reinterpret_cast<FT_BitmapGlyph>(glyph);
loadCharTexture(face, glyph_bitmap, character);
font->strokeCharacters[c] = character;
FT_Done_Glyph(glyph);
}
FT_Stroker_Done(stroker);
FT_Done_Face(face);
return font;
}
void drawText(std::shared_ptr<Font> font, std::string text, float x, float y, float scale, glm::vec3 color, bool drawStroke) {
// activate corresponding render state
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
fontShader->use();
fontShader->set("textColor", color);
fontShader->set("text", 0);
glActiveTexture(GL_TEXTURE0);
glm::mat4 projection = glm::ortho(0.0f, (float)viewportWidth, 0.0f, (float)viewportHeight);
fontShader->set("projection", projection);
// This is not in the learnopengl guide but it is VERY important
// I'm surprised I missed it but honestly... not so much. I'm an idiot
glBindVertexArray(textVAO);
// iterate through all characters
for (size_t i = 0; i < text.size(); i++) {
unsigned char c = text[i];
Character ch = drawStroke ? font->strokeCharacters[c] : font->characters[c];
float xpos = x + ch.bearing.x * scale;
float ypos = viewportHeight - y - font->height - (ch.size.y - ch.bearing.y) * scale;
float w = ch.size.x * scale;
float h = ch.size.y * scale;
// render glyph texture over quad
glBindTexture(GL_TEXTURE_2D, ch.ID);
float vertices[6][4] = {
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos, ypos, 0.0f, 1.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos + w, ypos + h, 1.0f, 0.0f }
};
// render glyph texture over quad
glBindTexture(GL_TEXTURE_2D, ch.ID);
// update content of VBO memory
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// render quad
glDrawArrays(GL_TRIANGLES, 0, 6);
// now advance cursors for next glyph (note that advance is number of 1/64 pixels)
x += (ch.advance >> 6) * scale; // bitshift by 6 to get value in pixels (2^6 = 64)
}
glBindTexture(GL_TEXTURE_2D, 0);
}
float calcTextWidth(std::shared_ptr<Font> font, std::string text, float scale) {
float x = 0;
// iterate through all characters
for (size_t i = 0; i < text.size(); i++) {
unsigned char c = text[i];
Character ch = font->characters[c];
x += (ch.advance >> 6) * scale;
}
return x;
}

26
core/src/rendering/font.h Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <memory>
#include <string>
#include <glm/glm.hpp>
// https://learnopengl.com/In-Practice/Text-Rendering
struct Character {
unsigned int ID; // ID handle of the glyph texture
glm::ivec2 size; // Size of glyph
glm::ivec2 bearing; // Offset from baseline to left/top of glyph
unsigned int advance; // Offset to advance to next glyph
};
struct Font {
unsigned int height;
Character characters[128];
Character strokeCharacters[128];
};
void fontInit();
void fontFinish();
std::shared_ptr<Font> loadFont(std::string fontName);
void drawText(std::shared_ptr<Font> font, std::string text, float x, float y, float scale=1.f, glm::vec3 color = glm::vec3(1,1,1), bool drawStroke = false);
float calcTextWidth(std::shared_ptr<Font> font, std::string text, float scale = 1.f);

View file

@ -1,5 +1,4 @@
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GL/gl.h>
#include <cmath>
#include <cstdio>
@ -21,8 +20,11 @@
#include "datatypes/vector.h"
#include "handles.h"
#include "math_helper.h"
#include "objects/hint.h"
#include "objects/message.h"
#include "objects/service/selection.h"
#include "partassembly.h"
#include "rendering/font.h"
#include "rendering/mesh2d.h"
#include "rendering/texture.h"
#include "rendering/torus.h"
@ -46,23 +48,26 @@ Shader* identityShader = NULL;
Shader* ghostShader = NULL;
Shader* wireframeShader = NULL;
Shader* outlineShader = NULL;
Shader* fontShader = NULL;
Shader* debugFontShader = NULL;
Shader* generic2dShader = NULL;
extern Camera camera;
Skybox* skyboxTexture = NULL;
Texture3D* studsTexture = NULL;
Texture* fontTexture = NULL;
Texture* debugFontTexture = NULL;
Mesh2D* rect2DMesh = NULL;
std::shared_ptr<Font> sansSerif;
bool debugRendererEnabled = false;
bool wireframeRendering = false;
int viewportWidth, viewportHeight;
void renderDebugInfo();
void drawRect(int x, int y, int width, int height, glm::vec3 color);
void drawRect(int x, int y, int width, int height, glm::vec4 color);
inline void drawRect(int x, int y, int width, int height, glm::vec3 color) { return drawRect(x, y, width, height, glm::vec4(color, 1)); };
void renderInit(GLFWwindow* window, int width, int height) {
void renderInit(int width, int height) {
viewportWidth = width, viewportHeight = height;
glViewport(0, 0, width, height);
@ -74,7 +79,7 @@ void renderInit(GLFWwindow* window, int width, int height) {
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
fontTexture = new Texture("assets/textures/debugfnt.bmp", GL_RGB);
debugFontTexture = new Texture("assets/textures/debugfnt.bmp", GL_RGB);
skyboxTexture = new Skybox({
"assets/textures/skybox/null_plainsky512_lf.jpg",
@ -95,7 +100,7 @@ void renderInit(GLFWwindow* window, int width, int height) {
ghostShader = new Shader("assets/shaders/ghost.vs", "assets/shaders/ghost.fs");
wireframeShader = new Shader("assets/shaders/wireframe.vs", "assets/shaders/wireframe.fs");
outlineShader = new Shader("assets/shaders/outline.vs", "assets/shaders/outline.fs");
fontShader = new Shader("assets/shaders/font.vs", "assets/shaders/font.fs");
debugFontShader = new Shader("assets/shaders/debug/debugfont.vs", "assets/shaders/debug/debugfont.fs");
generic2dShader = new Shader("assets/shaders/generic2d.vs", "assets/shaders/generic2d.fs");
// Create mesh for 2d rectangle
@ -110,6 +115,10 @@ void renderInit(GLFWwindow* window, int width, int height) {
};
rect2DMesh = new Mesh2D(6, rectVerts);
// Initialize fonts
fontInit();
sansSerif = loadFont("LiberationSans-Regular.ttf");
}
void renderParts() {
@ -356,7 +365,6 @@ void renderHandles() {
glm::vec4 screenPos = projection * view * glm::vec4((glm::vec3)cframe.Position(), 1.0f);
screenPos /= screenPos.w;
screenPos += 1; screenPos /= 2; screenPos.y = 1 - screenPos.y; screenPos *= glm::vec4(glm::vec2(viewportWidth, viewportHeight), 1, 1);
printVec((glm::vec3)screenPos);
drawRect(screenPos.x - 3, screenPos.y - 3, 6, 6, glm::vec3(0, 1, 1));
}
@ -572,6 +580,7 @@ void renderRotationArcs() {
handleShader->set("viewPos", camera.cameraPos);
PartAssembly assembly = PartAssembly::FromSelection(gDataModel->GetService<Selection>());
if (assembly.size() == Vector3::ZERO) return; // No parts are selected
for (HandleFace face : HandleFace::Faces) {
if (glm::any(glm::lessThan(face.normal, glm::vec3(0)))) continue;
@ -643,8 +652,35 @@ void addDebugRenderCFrame(CFrame frame, Color3 color) {
DEBUG_CFRAMES.push_back(std::make_pair(frame, color));
}
void renderMessages() {
glDisable(GL_DEPTH_TEST);
// glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
// glEnable(GL_BLEND);
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) {
if (!it->IsA<Message>()) continue;
std::shared_ptr<Message> message = it->CastTo<Message>().expect();
float textWidth = calcTextWidth(sansSerif, message->text);
// Render hint
if (message->GetClass() == &Hint::TYPE) {
drawRect(0, 0, viewportWidth, 20, glm::vec4(0,0,0,1));
drawText(sansSerif, message->text, (viewportWidth - textWidth) / 2, 0);
} else {
// Don't draw if text is empty
if (message->text == "") continue;
drawRect(0, 0, viewportWidth, viewportHeight, glm::vec4(0.5));
drawText(sansSerif, message->text, ((float)viewportWidth - textWidth) / 2, ((float)viewportHeight - sansSerif->height) / 2);
}
}
}
tu_time_t renderTime;
void render(GLFWwindow* window) {
void render() {
tu_time_t startTime = tu_clock_micros();
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@ -662,12 +698,14 @@ void render(GLFWwindow* window) {
renderWireframe();
if (debugRendererEnabled)
renderDebugInfo();
renderMessages();
// TODO: Make this a debug flag
// renderAABB();
renderTime = tu_clock_micros() - startTime;
}
void drawRect(int x, int y, int width, int height, glm::vec3 color) {
void drawRect(int x, int y, int width, int height, glm::vec4 color) {
// GL_CULL_FACE has to be disabled as we are flipping the order of the vertices here, besides we don't really care about it
glDisable(GL_CULL_FACE);
glm::mat4 model(1.0f); // Same applies to this VV

View file

@ -1,13 +1,12 @@
#pragma once
#include <GLFW/glfw3.h>
extern bool wireframeRendering;
class CFrame;
class Color3;
void renderInit(GLFWwindow* window, int width, int height);
void render(GLFWwindow* window);
void renderInit(int width, int height);
void render();
void setViewport(int width, int height);
void addDebugRenderCFrame(CFrame);
void addDebugRenderCFrame(CFrame, Color3);

View file

@ -7,7 +7,42 @@
struct vec { float x; float y; float z; };
void genTorusPoint(float outerRadius, float innerRadius, float tubeSides, float ringSides, int tube, int ring) {
unsigned int torusVAO, torusVBO;
int lastSize = 0;
void initTorus(int tubeSides, int ringSides) {
// Free existing buffer
if (torusVAO != 0)
glDeleteVertexArrays(1, &torusVAO);
if (torusVBO != 0)
glDeleteBuffers(1, &torusVBO);
lastSize = tubeSides * ringSides;
// Set up buffer
glGenVertexArrays(1, &torusVAO);
glBindVertexArray(torusVAO);
glGenBuffers(1, &torusVBO);
glBindBuffer(GL_ARRAY_BUFFER, torusVBO);
// Dynamic, because we update the vertices often V~~~~~~~~~~~~~~
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8 * 6 * tubeSides * ringSides, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(0 * sizeof(float)));
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void genTorusPoint(float* vertex, float outerRadius, float innerRadius, float tubeSides, float ringSides, int tube, int ring) {
float angle = float(tube) / tubeSides * 2 * PI;
float xL = innerRadius * cos(angle);
float yL = innerRadius * sin(angle);
@ -17,31 +52,41 @@ void genTorusPoint(float outerRadius, float innerRadius, float tubeSides, float
float y = (outerRadius + xL) * sin(outerAngle);
float z = yL;
// return vec { x, y, z };
glVertex3f(x, y, z);
vertex[0] = x; vertex[1] = y; vertex[2] = z;
// TODO: Add normals and tex coords
}
// made by yours truly
void genTorus(float outerRadius, float innerRadius, int tubeSides, int ringSides) {
glBegin(GL_TRIANGLES);
// Automatically generate VBO ad-hoc
if (lastSize != tubeSides * ringSides)
initTorus(tubeSides, ringSides);
float* vertices = new float[(tubeSides * ringSides * 6) * 8];
int vi = 0;
for (int i = 0; i < tubeSides; i++) {
float tube = float(i) / tubeSides;
for (int j = 0; j < ringSides; j++) {
float ring = float(j) / ringSides;
int in = (i+1) % tubeSides;
int jn = (j+1) % tubeSides;
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, i, j);
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, in, j);
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, in, jn);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, i, j);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, in, j);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, in, jn);
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, in, jn);
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, i, jn);
genTorusPoint(outerRadius, innerRadius, tubeSides, ringSides, i, j);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, in, jn);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, i, jn);
genTorusPoint(&vertices[8*vi++], outerRadius, innerRadius, tubeSides, ringSides, i, j);
}
}
glEnd();
// Bind new data
glBindBuffer(GL_ARRAY_BUFFER, torusVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * 8 * 6 * tubeSides * ringSides, vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Draw
glBindVertexArray(torusVAO);
glDrawArrays(GL_TRIANGLES, 0, tubeSides * ringSides * 6);
}

View file

@ -8,4 +8,5 @@ qt6
reactphysics3d
pugixml
luajit
qscintilla
qscintilla
freetype2

View file

@ -38,6 +38,8 @@ set(PROJECT_SOURCES
panes/outputtextview.cpp
script/scriptdocument.h
script/scriptdocument.cpp
script/commandedit.h
script/commandedit.cpp
aboutdialog.ui
aboutdialog.h
aboutdialog.cpp
@ -98,14 +100,6 @@ add_custom_command(
if (WIN32)
#include("${QT_DEPLOY_SUPPORT}")
# TODO: Add other translations
add_custom_command(
TARGET editor POST_BUILD
COMMAND ${WINDEPLOYQT_EXECUTABLE} $<TARGET_FILE:editor> --translations en --no-compiler-runtime --no-opengl-sw --no-system-d3d-compiler --plugindir $<TARGET_FILE_DIR:editor>/qtplugins
)
# No sense adding opengl-sw given that hardware acceleration is necessary, anyway
# Also don't want to clutter with plugins, add only needed ones
# Copy over QScintilla DLLs
# TODO: Use a better approach?
add_custom_command(
@ -113,6 +107,15 @@ if (WIN32)
COMMAND ${CMAKE_COMMAND} -E copy ${QSCINTILLA_DLLS} $<TARGET_FILE_DIR:editor>
)
# TODO: Add other translations
add_custom_command(
TARGET editor POST_BUILD
COMMAND ${WINDEPLOYQT_EXECUTABLE} $<TARGET_FILE:editor> ${QSCINTILLA_DLLS} --dir $<TARGET_FILE_DIR:editor> --translations en --no-compiler-runtime --no-opengl-sw --no-system-d3d-compiler --plugindir $<TARGET_FILE_DIR:editor>/qtplugins
)
# No sense adding opengl-sw given that hardware acceleration is necessary, anyway
# Also don't want to clutter with plugins, add only needed ones
# Copy qt.conf to override default plugins location
add_custom_command(
TARGET editor POST_BUILD

View file

@ -1 +1,17 @@
#pragma once
#pragma once
inline bool isDarkMode() {
// https://stackoverflow.com/a/78854851/16255372
#if defined(_WIN32)
// Never read dark theme on Windows as the app currently renders with white color palette regardless
return false;
#elif QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
const auto scheme = QGuiApplication::styleHints()->colorScheme();
return scheme == Qt::ColorScheme::Dark;
#else
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
return text.lightness() > window.lightness();
#endif // QT_VERSION
}

View file

@ -35,7 +35,7 @@ MainGLWidget::MainGLWidget(QWidget* parent): QOpenGLWidget(parent), contextMenu(
void MainGLWidget::initializeGL() {
glewInit();
renderInit(NULL, width(), height());
renderInit(width(), height());
}
inline void playSound(QString path) {
@ -63,7 +63,7 @@ extern std::weak_ptr<Part> draggingObject;
extern std::optional<HandleFace> draggingHandle;
extern Shader* shader;
void MainGLWidget::paintGL() {
::render(NULL);
::render();
}
bool isMouseRightDragging = false;

View file

@ -1,5 +1,6 @@
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "script/commandedit.h"
#include "common.h"
#include "aboutdialog.h"
#include "logger.h"
@ -14,6 +15,7 @@
#include <qevent.h>
#include <qglobal.h>
#include <qkeysequence.h>
#include <qlabel.h>
#include <qmessagebox.h>
#include <qmimedata.h>
#include <qnamespace.h>
@ -74,10 +76,16 @@ MainWindow::MainWindow(QWidget *parent)
ui->actionRedo->setShortcuts({QKeySequence("Ctrl+Shift+Z"), QKeySequence("Ctrl+Y")});
QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() + QStringList { "./assets/icons" });
// Force theme under windows
#ifdef _WIN32
QIcon::setThemeName("editor");
#else
if (isDarkMode())
QIcon::setFallbackThemeName("editor-dark");
else
QIcon::setThemeName("editor");
QIcon::setFallbackThemeName("editor");
#endif
// qApp->setStyle(QStyleFactory::create("fusion"));
defaultMessageHandler = qInstallMessageHandler(logQtMessage);
@ -111,6 +119,8 @@ MainWindow::MainWindow(QWidget *parent)
undoManager.SetUndoStateListener([&]() {
updateToolbars();
});
setUpCommandBar();
}
void MainWindow::closeEvent(QCloseEvent* evt) {
@ -136,6 +146,14 @@ void MainWindow::closeEvent(QCloseEvent* evt) {
#endif
}
void MainWindow::setUpCommandBar() {
CommandEdit* commandEdit;
QToolBar* commandBar = ui->commandBar;
commandBar->layout()->setSpacing(5);
commandBar->addWidget(new QLabel(tr("Command ")));
commandBar->addWidget(commandEdit = new CommandEdit());
}
void MainWindow::connectActionHandlers() {
connect(ui->actionToolSelect, &QAction::triggered, this, [&]() { selectedTool = TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolMove, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_MOVE : TOOL_SELECT; updateToolbars(); });
@ -592,19 +610,23 @@ ScriptDocument* MainWindow::findScriptWindow(std::shared_ptr<Script> script) {
return nullptr;
}
void MainWindow::openScriptDocument(std::shared_ptr<Script> script) {
void MainWindow::openScriptDocument(std::shared_ptr<Script> script, int line) {
// Document already exists, don't open it
ScriptDocument* doc = findScriptWindow(script);
if (doc != nullptr) {
ui->mdiArea->setActiveSubWindow(doc);
doc->setFocus();
if (line > -1) doc->moveCursor(line);
return;
}
doc = new ScriptDocument(script);
doc->setAttribute(Qt::WA_DeleteOnClose, true);
if (line > -1) doc->moveCursor(line);
ui->mdiArea->addSubWindow(doc);
ui->mdiArea->setActiveSubWindow(doc);
doc->showMaximized();
doc->setFocus();
}
void MainWindow::closeScriptDocument(std::shared_ptr<Script> script) {

View file

@ -58,7 +58,7 @@ public:
GridSnappingMode snappingMode;
bool editSoundEffects = true;
void openScriptDocument(std::shared_ptr<Script>);
void openScriptDocument(std::shared_ptr<Script>, int line = -1);
void closeScriptDocument(std::shared_ptr<Script>);
void openFile(std::string path);
@ -69,11 +69,11 @@ public:
private:
PlaceDocument* placeDocument;
void setUpCommandBar();
void connectActionHandlers();
void updateToolbars();
void closeEvent(QCloseEvent* evt) override;
ScriptDocument* findScriptWindow(std::shared_ptr<Script>);
void connectActionHandlers();
std::optional<std::string> openFileDialog(QString filter, QString defaultExtension, QFileDialog::AcceptMode acceptMode, QString title = "");
};

View file

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
<string>Openblocks Editor</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
@ -172,7 +172,7 @@
</widget>
<widget class="QToolBar" name="editTools">
<property name="windowTitle">
<string>toolBar_3</string>
<string>Edit Tools</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
@ -237,7 +237,7 @@
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
<string>Sound Controls</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
@ -247,6 +247,17 @@
</attribute>
<addaction name="actionToggleEditSounds"/>
</widget>
<widget class="QToolBar" name="commandBar">
<property name="windowTitle">
<string>Command Bar</string>
</property>
<attribute name="toolBarArea">
<enum>BottomToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<action name="actionAddPart">
<property name="icon">
<iconset>
@ -306,8 +317,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="edit-select">
<normaloff>assets/icons/editor/drag.png</normaloff>assets/icons/editor/drag.png</iconset>
<iconset theme="edit-select"/>
</property>
<property name="text">
<string>Select Objects</string>

View file

@ -1,6 +1,6 @@
#include "outputtextview.h"
#include "logger.h"
#include "mainwindow.h"
#include "objects/script.h"
#include "panes/outputtextview.h"
#include <QEvent>
#include <QTextEdit>
@ -12,8 +12,7 @@
#include <string>
OutputTextView::OutputTextView(QWidget* parent) : QTextEdit(parent) {
Logger::addLogListener(std::bind(&OutputTextView::handleLog, this, std::placeholders::_1, std::placeholders::_2));
Logger::addLogListener(std::bind(&OutputTextView::handleLogTrace, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
Logger::addLogListener(std::bind(&OutputTextView::handleLog, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
ensureCursorVisible();
QFont font("");
@ -36,13 +35,9 @@ OutputTextView::OutputTextView(QWidget* parent) : QTextEdit(parent) {
OutputTextView::~OutputTextView() = default;
void OutputTextView::handleLog(Logger::LogLevel logLevel, std::string message) {
void OutputTextView::handleLog(Logger::LogLevel logLevel, std::string message, Logger::ScriptSource source) {
if (logLevel == Logger::LogLevel::DEBUG) return;
// Skip if trace, as that is handled by handleLogTrace
if (logLevel == Logger::LogLevel::TRACE && !message.starts_with("Stack"))
return;
// https://stackoverflow.com/a/61722734/16255372
moveCursor(QTextCursor::MoveOperation::End);
QTextCursor cursor = textCursor();
@ -58,24 +53,13 @@ void OutputTextView::handleLog(Logger::LogLevel logLevel, std::string message) {
format.setFontWeight(QFont::Bold);
}
cursor.insertText(message.c_str(), format);
cursor.insertText("\n", QTextCharFormat());
}
void OutputTextView::handleLogTrace(std::string message, std::string source, int line, void* userData) {
std::weak_ptr<Script>* script = (std::weak_ptr<Script>*)userData;
// https://stackoverflow.com/a/61722734/16255372
QTextCursor cursor = textCursor();
QTextCharFormat format = cursor.charFormat();
format.setForeground(QColor(0, 127, 255));
if (userData != nullptr && !script->expired()) {
// Add anchor point if source is provided
if (source.script != nullptr) {
int id = stackTraceScriptsLastId++;
stackTraceScripts[id] = *script;
stackTraceScripts[id] = source.script;
format.setAnchor(true);
format.setAnchorHref(QString::number(id));
format.setAnchorHref(QString::number(id) + ":" + QString::number(source.line));
}
cursor.insertText(message.c_str(), format);
@ -87,11 +71,13 @@ void OutputTextView::mousePressEvent(QMouseEvent *e) {
QString anchor = anchorAt(e->pos());
if (anchor == "" || e->modifiers() & Qt::AltModifier) return QTextEdit::mousePressEvent(e);
auto script = stackTraceScripts[anchor.toInt()];
int idx = anchor.indexOf(":");
int id = anchor.mid(0, idx).toInt(), line = anchor.mid(idx+1).toInt();
auto script = stackTraceScripts[id];
if (script.expired()) return QTextEdit::mousePressEvent(e);
MainWindow* mainWnd = dynamic_cast<MainWindow*>(window());
mainWnd->openScriptDocument(script.lock());
mainWnd->openScriptDocument(script.lock(), line);
}
void OutputTextView::mouseReleaseEvent(QMouseEvent *e) {

View file

@ -14,8 +14,7 @@ private:
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void handleLog(Logger::LogLevel, std::string);
void handleLogTrace(std::string, std::string, int, void*);
void handleLog(Logger::LogLevel, std::string, Logger::ScriptSource source);
std::map<int, std::weak_ptr<Script>> stackTraceScripts;
int stackTraceScriptsLastId = 0;

View file

@ -15,7 +15,6 @@
#include <QStyledItemDelegate>
#include <QPainter>
#include <QTime>
#include <cfloat>
#include <cmath>
#include <functional>
#include <qapplication.h>
@ -24,6 +23,14 @@
#include <qnamespace.h>
#include <qtreewidget.h>
QDoubleSpinBox* makeDoubleSpinBox(QWidget* parent = nullptr) {
QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
spinBox->setMaximum(INFINITY);
spinBox->setMinimum(-INFINITY);
spinBox->setDecimals(4);
return spinBox;
}
class PropertiesItemDelegate : public QStyledItemDelegate {
PropertiesView* view;
public:
@ -64,7 +71,7 @@ public:
Vector3 vector = currentValue.get<Vector3>();
float value = componentName == "X" ? vector.X() : componentName == "Y" ? vector.Y() : componentName == "Z" ? vector.Z() : 0;
QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
QDoubleSpinBox* spinBox = makeDoubleSpinBox(parent);
spinBox->setValue(value);
return spinBox;
@ -75,9 +82,9 @@ public:
if (meta.type.descriptor == &FLOAT_TYPE) {
QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
spinBox->setValue(currentValue.get<float>());
spinBox->setMinimum(-INFINITY);
spinBox->setMaximum(INFINITY);
spinBox->setValue(currentValue.get<float>());
if (meta.flags & PROP_UNIT_FLOAT) {
spinBox->setMinimum(0);
@ -221,7 +228,8 @@ public:
: componentName == "Z" ? Vector3(prev.X(), prev.Y(), value) : prev;
inst->SetProperty(propertyName, newVector).expect();
view->rebuildCompositeProperty(view->itemFromIndex(index.parent()), &Vector3::TYPE, newVector);
// SetProperty above already causes the composite to be rebuilt. So we get rid of it here to prevent errors
// view->rebuildCompositeProperty(view->itemFromIndex(index.parent()), &Vector3::TYPE, newVector);
return;
}

View file

@ -0,0 +1,115 @@
#include "commandedit.h"
#include "common.h"
#include "logger.h"
#include "lua.h"
#include "objects/service/script/scriptcontext.h"
#include "luaapis.h" // IWYU pragma: keep
#include <qevent.h>
#include <qlineedit.h>
#include <qnamespace.h>
int script_errhandler(lua_State*);
CommandEdit::CommandEdit(QWidget* parent) : QLineEdit(parent) {
connect(this, &QLineEdit::returnPressed, this, &CommandEdit::executeCommand);
}
CommandEdit::~CommandEdit() = default;
void CommandEdit::executeCommand() {
std::string command = this->text().toStdString();
// Output
Logger::infof("> %s", command.c_str());
// Select all so that the user can type over it
this->selectAll();
// Execute via Lua
auto context = gDataModel->GetService<ScriptContext>();
lua_State* L = context->state;
int top = lua_gettop(L);
lua_State* Lt = lua_newthread(L);
// Push wrapper as thread function
lua_getfield(Lt, LUA_REGISTRYINDEX, "LuaPCallWrapper");
// Load source code and push onto thread as upvalue for wrapper
int status = luaL_loadstring(Lt, command.c_str());
if (status != LUA_OK) {
// Failed to parse/load chunk
Logger::error(lua_tostring(Lt, -1));
lua_settop(L, top);
return;
}
getOrCreateEnvironment(Lt);
lua_setfenv(Lt, -2); // Set env of loaded function
// Push our error handler and then generate the wrapped function
lua_pushcfunction(Lt, script_errhandler);
lua_call(Lt, 2, 1);
// Resume the thread
lua_resume(Lt, 0);
lua_pop(L, 1); // Pop the thread
lua_settop(L, top);
// Push to history
if (commandHistory.size() == 0 || commandHistory.back() != command) {
historyIndex = commandHistory.size();
commandHistory.push_back(command);
}
};
// Gets the command bar environment from the registry, or creates a new one and registers it
void CommandEdit::getOrCreateEnvironment(lua_State* L) {
auto context = gDataModel->GetService<ScriptContext>();
// Try to find existing environment
lua_getfield(L, LUA_REGISTRYINDEX, "commandBarEnv");
if (!lua_isnil(L, -1))
return; // Return the found environment
lua_pop(L, 1); // Pop nil
// Initialize script globals
context->NewEnvironment(L); // Pushes envtable, metatable
// Set source in metatable
lua_pushstring(L, "commandbar");
lua_setfield(L, -2, "source");
lua_pop(L, 1); // Pop metatable
// Register it
lua_pushvalue(L, -1); // Copy
lua_setfield(L, LUA_REGISTRYINDEX, "commandBarEnv");
// Remainder on stack:
// 1. Env table
}
void CommandEdit::keyPressEvent(QKeyEvent* evt) {
switch (evt->key()) {
case Qt::Key_Up:
if (historyIndex > 0)
historyIndex--;
if (commandHistory.size() > 0)
this->setText(QString::fromStdString(commandHistory[historyIndex]));
return;
case Qt::Key_Down:
if (historyIndex+1 < (int)commandHistory.size())
historyIndex++;
if (commandHistory.size() > 0)
this->setText(QString::fromStdString(commandHistory[historyIndex]));
return;
}
return QLineEdit::keyPressEvent(evt);
}

View file

@ -0,0 +1,21 @@
#pragma once
#include <qlineedit.h>
#include <vector>
struct lua_State;
class CommandEdit : public QLineEdit {
Q_OBJECT
std::vector<std::string> commandHistory;
int historyIndex = 0;
void executeCommand();
void getOrCreateEnvironment(lua_State* L);
public:
CommandEdit(QWidget* parent = nullptr);
~CommandEdit();
void keyPressEvent(QKeyEvent *) override;
};

View file

@ -14,27 +14,15 @@
#include <qglobal.h>
#include <qlayout.h>
#include <qtextformat.h>
#include "mainwindow.h"
#include "objects/script.h"
#include "datatypes/variant.h"
#include <QPalette>
#include <QStyleHints>
#include "mainwindow.h"
#include "editorcommon.h"
#include "objects/script.h"
#include "datatypes/variant.h"
QsciAPIs* makeApis(QsciLexer*);
inline bool isDarkMode() {
// https://stackoverflow.com/a/78854851/16255372
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
const auto scheme = QGuiApplication::styleHints()->colorScheme();
return scheme == Qt::ColorScheme::Dark;
#else
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
return text.lightness() > window.lightness();
#endif // QT_VERSION
}
std::map<int, const QColor> DARK_MODE_COLOR_SCHEME = {{
{QsciLexerLua::Comment, QColor("#808080")},
{QsciLexerLua::LineComment, QColor("#808080")},
@ -71,7 +59,7 @@ class ObLuaLexer : public QsciLexerLua {
//"foreach foreachi getn "
// Openblocks extensions
"shared require game workspace "
"shared require game workspace Instance Instance.new "
"_G _VERSION getfenv getmetatable ipairs loadstring "
"next pairs pcall rawequal rawget rawset select "
@ -151,7 +139,8 @@ ScriptDocument::ScriptDocument(std::shared_ptr<Script> script, QWidget* parent):
scintilla->setCaretForegroundColor(palette().text().color());
scintilla->setFont(font);
scintilla->setTabWidth(4);
scintilla->setEolMode(QsciScintilla::EolUnix); // LF endings
scintilla->setText(QString::fromStdString(script->source));
ObLuaLexer* lexer = new ObLuaLexer;
@ -190,6 +179,13 @@ ScriptDocument::ScriptDocument(std::shared_ptr<Script> script, QWidget* parent):
ScriptDocument::~ScriptDocument() {
}
void ScriptDocument::moveCursor(int line) {
if (line == -1) return;
int lineLength = scintilla->lineLength(line-1);
scintilla->setCursorPosition(line-1, lineLength-1);
}
QsciAPIs* makeApis(QsciLexer* lexer) {
QsciAPIs* apis = new QsciAPIs(lexer);

View file

@ -17,4 +17,5 @@ public:
~ScriptDocument() override;
inline std::shared_ptr<Script> getScript() { return script; }
void moveCursor(int line);
};

View file

@ -8,7 +8,8 @@
"stb",
"reactphysics3d",
"pkgconf",
"luajit"
"luajit",
"freetype"
],
"overrides": [
{