initial commit

This commit is contained in:
2025-01-05 02:53:58 +00:00
commit 77fc0bd9ff
10 changed files with 15189 additions and 0 deletions

6
.clangd Normal file
View File

@@ -0,0 +1,6 @@
CompileFlags:
Add:
- -I/Users/kennethng/Projects/imdraw/vendor/cimgui
- -I/Users/kennethng/Projects/imdraw/vendor/imgui
- -I/Users/kennethng/Projects/imdraw/vendor/sokol
- -I/Users/kennethng/Projects/imdraw/vendor/sokol/util

81
.gitignore vendored Normal file
View File

@@ -0,0 +1,81 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,linux,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,windows
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows
build/

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "vendor/sokol"]
path = vendor/sokol
url = https://github.com/floooh/sokol.git
[submodule "vendor/cimgui"]
path = vendor/cimgui
url = https://github.com/cimgui/cimgui.git

49
build.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -eu
# make sure the cwd is where the script is.
pushd "$(dirname "$0")" >> /dev/null
CC=gcc
AR=ar
src=(
src/main.c
)
cimgui_flags="-L./vendor/cimgui -lcimgui -I./vendor/cimgui"
sokol_flags="-L./build -lsokol -I./vendor/sokol -I./vendor/sokol/util"
compile_flags="-o ./build/imdraw -framework QuartzCore -framework Cocoa -framework MetalKit -framework Metal"
mkdir -p build/
# compile cimgui into a static library
if [ ! -f vendor/cimgui/libcimgui.a ]; then
echo "Compiling cimgui..."
pushd vendor/cimgui >> /dev/null
make static
popd >> /dev/null
echo "cimgui compiled!"
else
echo "libcimgui exists at vendor/cimgui, skipping compilation!"
fi
# compile sokol into a static library
if [ ! -f build/libsokol.a ]; then
echo "Compiling Sokol..."
$CC -c -x objective-c lib/sokol.c -o build/sokol.o $cimgui_flags $sokol_flags
$AR rcs build/libsokol.a build/sokol.o
rm build/sokol.o
echo "Sokol compiled!"
else
echo "libsokol.a exists, skipping compilation!"
fi
compile_cmd="$CC $src $compile_flags $cimgui_flags $sokol_flags"
echo $compile_cmd
$compile_cmd
popd >> /dev/null

18
configure.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -eu
script_path="$(realpath $0)"
root="$(dirname $script_path)"
pushd "$(dirname "$0")" > /dev/null
cat > .clangd <<- EOF
CompileFlags:
Add:
- -I$root/vendor/cimgui
- -I$root/vendor/imgui
- -I$root/vendor/sokol
- -I$root/vendor/sokol/util
EOF
popd > /dev/null

22
lib/sokol.c Normal file
View File

@@ -0,0 +1,22 @@
// sokol implementation library on non-Apple platforms
#define SOKOL_IMPL
#if defined(__MINGW32__)
#define SOKOL_GLCORE
#elif defined(_WIN32)
#define SOKOL_D3D11
#elif defined(__EMSCRIPTEN__)
#define SOKOL_GLES3
#elif defined(__APPLE__)
// NOTE: on macOS, sokol.c is compiled explicitely as ObjC
#define SOKOL_METAL
#else
#define SOKOL_GLCORE
#endif
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#include "cimgui.h"
#define SOKOL_IMGUI_IMPL
#include "sokol_imgui.h"

14496
src/fa_regular_400.h Normal file

File diff suppressed because it is too large Load Diff

509
src/main.c Normal file
View File

@@ -0,0 +1,509 @@
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#define SOKOL_METAL
#include "cimgui.h"
#include "fa_regular_400.h"
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_imgui.h"
#include "sokol_log.h"
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// if mouse movement between mouse down and mouse up is below this threshold (in
// px) then it is considered to be a mouse click
#define CLICK_THRESHOLD 1
// if the distance between mouse pos when clicked and the entity is below this
// threshold then the entity is considered to be selected
#define SELECT_THRESHOLD 25
#define ARENA_INITIAL_SIZE 1000
// ============================================================================
// struct definitions
// ============================================================================
// ===========================
// struct: arena
// ===========================
typedef struct arena_page {
void *data;
size_t end_ptr;
struct arena_page *next;
} arena_page_t;
typedef struct {
arena_page_t *page;
size_t capacity;
} arena_t;
arena_page_t *arena_new_page(size_t page_size) {
arena_page_t *new_page = malloc(sizeof(arena_page_t));
new_page->data = malloc(sizeof(char) * page_size);
new_page->next = NULL;
new_page->end_ptr = 0;
return new_page;
}
arena_t *arena_alloc(size_t size) {
arena_t *arena = malloc(sizeof(arena_t));
arena->page = arena_new_page(size);
arena->capacity = size;
return arena;
}
void *arena_push(arena_t *arena, size_t size) {
if (arena->page->end_ptr + size >= arena->capacity) {
arena_page_t *new_page = arena_new_page(arena->capacity);
new_page->next = arena->page;
arena->page = new_page;
printf("arena realloc\n");
}
void *ptr = arena->page->data + arena->page->end_ptr;
arena->page->end_ptr += size;
return ptr;
}
void arena_free(arena_t *arena) {
for (arena_page_t *page = arena->page; page != NULL; page = page->next) {
free(page->data);
free(page);
}
free(arena);
}
// ===========================
// struct: point_list
// ===========================
typedef struct {
ImVec2 *items;
size_t length;
size_t capacity;
} point_list_t;
point_list_t point_list_alloc(size_t capacity) {
ImVec2 *items = malloc(sizeof(ImVec2) * capacity);
return (point_list_t){
.items = items,
.length = 0,
.capacity = capacity,
};
}
ImVec2 *point_list_push(point_list_t *list) {
if (list->length + 1 >= list->capacity) {
list->capacity *= 2;
list->items = realloc(list->items, sizeof(ImVec2) * list->capacity);
}
ImVec2 *item = list->items + list->length;
list->length += 1;
return item;
}
void point_list_clear(point_list_t *list) { list->length = 0; }
void point_list_copy(point_list_t *dest, point_list_t *src) {
memcpy(dest->items, src->items, sizeof(ImVec2) * src->capacity);
dest->capacity = src->capacity;
dest->length = src->length;
}
void point_list_free(point_list_t *list) { free(list->items); }
// ===========================
// struct: entity
// ===========================
typedef enum { entity_flag_rect = 1, entity_flag_path = 1 << 1 } entity_flag_t;
typedef struct entity {
entity_flag_t flags;
point_list_t points;
struct entity *next;
struct entity *prev;
} entity_t;
// ===========================
// struct: game state
// ===========================
typedef enum {
toolbox_button_select,
toolbox_button_draw,
toolbox_button_rectangle,
} toolbox_button_kind_t;
typedef struct {
ImFont *fa_font;
arena_t *arena;
entity_t *entites;
entity_t *freed_entity;
sg_pass_action pass_action;
bool is_dragging;
bool is_prev_dragging;
point_list_t points;
ImVec2 drag_start;
ImVec2 last_mouse_pos;
entity_t *selected_entity;
toolbox_button_kind_t selected_toolbox_button;
} state_t;
entity_t *entity_alloc(state_t *state, size_t point_count) {
entity_t *entity = state->freed_entity;
if (entity) {
state->freed_entity = state->freed_entity->next;
} else {
printf("pushing to arena\n");
entity = arena_push(state->arena, sizeof(entity_t));
entity->points = point_list_alloc(point_count);
}
entity->next = NULL;
entity->prev = NULL;
return entity;
}
void entity_recycle(state_t *state, entity_t *entity) {
entity->next = state->freed_entity;
entity->prev = NULL;
state->freed_entity = entity;
point_list_clear(&entity->points);
}
void entity_free(entity_t *entity) { point_list_free(&entity->points); }
void push_entity(state_t *state, entity_t *entity) {
entity->next = state->entites;
if (state->entites) {
state->entites->prev = entity;
}
state->entites = entity;
}
void remove_entity(state_t *state, entity_t *entity) {
if (state->entites == entity) {
state->entites = entity->next;
}
if (entity->prev) {
entity->prev->next = entity->next;
}
if (entity->next) {
entity->next->prev = entity->prev;
}
if (state->entites == NULL) {
for (entity_t *entity = state->freed_entity; entity != NULL;
entity = entity->next) {
entity_free(entity);
}
arena_free(state->arena);
state->arena = arena_alloc(ARENA_INITIAL_SIZE);
} else {
entity_recycle(state, entity);
}
}
// ============================================================================
// utils/helpers
// ============================================================================
float vec2_magnitude_sqr(const ImVec2 *v) { return v->x * v->x + v->y * v->y; }
float vec2_distance_sqr(const ImVec2 *v1, const ImVec2 *v2) {
return (v1->x - v2->x) * (v1->x - v2->x) + (v1->y - v2->y) * (v1->y - v2->y);
}
void project_point_to_segment(ImVec2 *out, const ImVec2 *v1, const ImVec2 *v2,
const ImVec2 *point) {
// Consider:
//
// a = vector v1 -> point
// b = vector v1 -> v2
// b^ = unit vector of b
//
// then:
//
// projection of a on b = (a dot b) / ||b||
// projection vector = (a dot b / ||b||)b^
// = (a dot b / b dot b)b
//
// resultant point from origin = v1 + projection vector
ImVec2 a = {
point->x - v1->x,
point->y - v1->y,
};
ImVec2 b = {
v2->x - v1->x,
v2->y - v1->y,
};
float a_dot_b = a.x * b.x + a.y * b.y;
// if dot product is less than zero, then the point is outside of segment v1v2
// closer to v1
if (a_dot_b < 0) {
out->x = v1->x;
out->y = v1->y;
return;
}
float b_dot_b = b.x * b.x + b.y * b.y;
// if a dot b > b dot b, then the projection magnitude > ||b||
// i.e. the point falls outside of segment v1v2 closer to v2
if (a_dot_b > b_dot_b) {
out->x = v2->x;
out->y = v2->y;
return;
}
float frac = a_dot_b / b_dot_b;
// v1 + projection vector = resultant
out->x = v1->x + frac * b.x;
out->y = v1->y + frac * b.y;
}
bool is_mouse_click(const ImVec2 *mouse_down_pos, const ImVec2 *mouse_up_pos) {
if (igIsMouseReleased_Nil(ImGuiMouseButton_Left)) {
return vec2_distance_sqr(mouse_up_pos, mouse_down_pos) <= CLICK_THRESHOLD;
}
return false;
}
// ============================================================================
// main program logic
// ============================================================================
static state_t state;
static void init(void) {
sg_setup(&(sg_desc){
.environment = sglue_environment(),
.logger.func = slog_func,
});
simgui_setup(&(simgui_desc_t){
.logger.func = slog_func,
});
struct ImGuiIO *io = igGetIO();
struct ImFontConfig *config = ImFontConfig_ImFontConfig();
config->MergeMode = true;
config->GlyphMinAdvanceX = 13.0f;
config->FontDataOwnedByAtlas = false;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA};
state.fa_font = ImFontAtlas_AddFontFromMemoryTTF(
io->Fonts, FA4_TTF, FA4_TTF_SIZE, 16.0f, config, icon_ranges);
state.arena = arena_alloc(ARENA_INITIAL_SIZE);
state.points = point_list_alloc(100);
state.last_mouse_pos.x = 0;
state.last_mouse_pos.y = 0;
state.is_dragging = false;
state.pass_action.colors[0].load_action = SG_LOADACTION_CLEAR;
state.selected_entity = NULL;
state.selected_toolbox_button = toolbox_button_select;
}
static void toolbox_window(void);
static void frame(void) {
simgui_new_frame(&(simgui_frame_desc_t){
.width = sapp_width(),
.height = sapp_height(),
.delta_time = sapp_frame_duration(),
.dpi_scale = sapp_dpi_scale(),
});
struct ImGuiViewport *viewport = igGetMainViewport();
igSetNextWindowPos(viewport->WorkPos, ImGuiCond_Always, (ImVec2){0, 0});
igSetNextWindowSize(viewport->WorkSize, ImGuiCond_Always);
igSetNextWindowViewport(viewport->ID);
igPushStyleVar_Float(ImGuiStyleVar_WindowRounding, 0.0f);
igBegin("canvas", 0,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImDrawList *draw_list = igGetWindowDrawList();
struct ImGuiIO *io = igGetIO();
igInvisibleButton("canvas", viewport->WorkSize, ImGuiButtonFlags_None);
if (igIsMouseDown_Nil(ImGuiMouseButton_Left)) {
if (!state.is_dragging) {
state.is_dragging = true;
state.is_prev_dragging = false;
state.drag_start = io->MousePos;
} else {
state.is_prev_dragging = true;
state.selected_entity = NULL;
}
} else {
state.is_dragging = false;
}
switch (state.selected_toolbox_button) {
default:
break;
case toolbox_button_select: {
if (is_mouse_click(&state.drag_start, &io->MousePos)) {
bool has_selected_entity = false;
for (entity_t *entity = state.entites; entity != NULL;
entity = entity->next) {
for (size_t i = 1; i < entity->points.length; ++i) {
ImVec2 *current_point = &entity->points.items[i];
ImVec2 *last_point = &entity->points.items[i - 1];
ImVec2 mouse_pos_proj;
project_point_to_segment(&mouse_pos_proj, last_point, current_point,
&io->MousePos);
ImVec2 mouse_pos_delta_to_segment = {
mouse_pos_proj.x - io->MousePos.x,
mouse_pos_proj.y - io->MousePos.y};
float magnitude_sqr = vec2_magnitude_sqr(&mouse_pos_delta_to_segment);
if (magnitude_sqr <= SELECT_THRESHOLD) {
has_selected_entity = true;
state.selected_entity = entity;
break;
}
}
}
if (!has_selected_entity) {
state.selected_entity = NULL;
}
}
break;
}
case toolbox_button_draw: {
if (state.is_dragging && (state.last_mouse_pos.x != io->MousePos.x ||
state.last_mouse_pos.y != io->MousePos.y)) {
*point_list_push(&state.points) = io->MousePos;
} else if (!state.is_dragging && state.is_prev_dragging &&
state.points.length > 0) {
entity_t *entity = entity_alloc(&state, state.points.capacity);
entity->flags = entity_flag_path;
point_list_copy(&entity->points, &state.points);
push_entity(&state, entity);
point_list_clear(&state.points);
state.is_prev_dragging = false;
}
for (size_t i = 1; i < state.points.length; ++i) {
ImDrawList_AddLine(draw_list, state.points.items[i - 1],
state.points.items[i], 0xFFFFFFFF, 2);
}
break;
}
}
if (igIsKeyPressed_Bool(ImGuiKey_Backspace, false)) {
if (state.entites) {
remove_entity(&state, state.selected_entity);
state.selected_entity = NULL;
}
}
//======= draw entities to canvas =========
for (entity_t *entity = state.entites; entity != NULL;
entity = entity->next) {
if (entity->flags & entity_flag_path) {
for (size_t i = 1; i < entity->points.length; ++i) {
ImVec2 *current_point = &entity->points.items[i];
ImVec2 *last_point = &entity->points.items[i - 1];
ImU32 color = entity == state.selected_entity ? 0xFF0000FF : 0xFFFFFFFF;
ImDrawList_AddLine(draw_list, *last_point, *current_point, color, 2);
}
}
}
igShowMetricsWindow(NULL);
state.last_mouse_pos = io->MousePos;
igSetNextWindowPos(viewport->WorkPos, ImGuiCond_None, (ImVec2){0, 0});
toolbox_window();
igEnd();
igPopStyleVar(1);
sg_begin_pass(&(sg_pass){
.action = state.pass_action,
.swapchain = sglue_swapchain(),
});
simgui_render();
sg_end_pass();
sg_commit();
}
static void toolbox_window(void) {
static ImVec2 button_size = {24, 24};
igBegin(ICON_FA_COG, 0, ImGuiWindowFlags_NoResize);
igPushStyleVar_Vec2(ImGuiStyleVar_ButtonTextAlign, (ImVec2){1, 1});
igPushStyleColor_U32(ImGuiCol_Button, 0);
if (igButton(ICON_FA_MOUSE_POINTER, button_size)) {
state.selected_toolbox_button = toolbox_button_select;
};
if (igButton(ICON_FA_PENCIL, button_size)) {
state.selected_toolbox_button = toolbox_button_draw;
};
igPushStyleVar_Vec2(ImGuiStyleVar_ButtonTextAlign, (ImVec2){0.8, 1});
igButton(ICON_FA_SQUARE_O, button_size);
igPopStyleVar(1);
igPopStyleVar(1);
igPopStyleColor(1);
igEnd();
}
static void cleanup(void) {
simgui_shutdown();
sg_shutdown();
}
static void event(const sapp_event *event) { simgui_handle_event(event); }
sapp_desc sokol_main(int argc, char *argv[]) {
return (sapp_desc){
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.logger.func = slog_func,
.width = 640,
.height = 480,
.window_title = "ImDraw",
.icon.sokol_default = true,
};
}

1
vendor/cimgui vendored Submodule

Submodule vendor/cimgui added at d9982af929

1
vendor/sokol vendored Submodule

Submodule vendor/sokol added at c1cc713a48