Fry3DX 0.4: an easy 3D modeller

Shapes with live settings, shape editing, knife and hole tools, drawing
shapes onto other objects, combining objects (add / subtract / overlap),
copies, thickness, painting with stickers and layers, a Layout tab, a
Print tab with checks, packed .fry files with autosave, export to GLB,
glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash
screen, and a website for GitHub Pages in docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 21:18:35 +02:00
commit f3e8eca87b
135 changed files with 23423 additions and 0 deletions

77
docs/content.js Normal file
View file

@ -0,0 +1,77 @@
// Everything the page says. To update the site, change this file only:
// add a feature to `features`, a picture to `shots`, a version to `versions` (newest first).
window.FRY = {
name: "Fry3DX",
long: "Frying 3D Extreme",
author: "Nexoniarz",
authorUrl: "https://github.com/Nexoniarz",
repo: "https://github.com/Nexoniarz/Fry3DX",
tagline: "A 3D modeller that does what you'd expect.",
intro: "Pick a tool, click, drag. Plain words instead of jargon, a toolbox like GIMP's, and the useful stuff from the big modellers without the hundred menus around it.",
why: {
title: "Why does this exist?",
text: [
"Because the well-known modellers suck at being easy. You open them and get a wall of panels, shortcuts nobody would ever guess, and modes inside modes. Making a box with a hole for a screw shouldn't need a tutorial.",
"So Nexoniarz made one that's built around how people actually think: pick a tool, click on the thing, drag. Every button says what it does, every tool has a tooltip, and the clever parts (rounding, mirroring, combining, checking a print) are one click away.",
],
},
features: [
{ icon: "▣", title: "Shapes that stay editable", text: "Cube, cylinder, sphere, torus, stairs, arch and more, plus starter models like a table, chair or barrel. Change their sizes any time in Properties." },
{ icon: "✎", title: "Draw a shape anywhere", text: "Click an outline on the ground or straight on the face of another object: it becomes a new part, gets added to it, or cuts into it." },
{ icon: "⊕", title: "Add, subtract, overlap", text: "Combine objects: add them together, take one out of another, or keep only where they overlap. The result is always closed and ready to print." },
{ icon: "◫", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." },
{ icon: "◎", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." },
{ icon: "▤", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." },
{ icon: "🖌", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." },
{ icon: "▦", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." },
{ icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." },
{ icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." },
{ icon: "🗀", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." },
{ icon: "💾", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." },
],
shots: [
{ img: "img/model.webp", title: "Model", text: "Starter models, a folder of parts, a shape drawn onto a cube's face, rendered with sky and shadows." },
{ img: "img/paint.webp", title: "Paint", text: "A picture pasted on as a sticker: it bends over the cube's edge like a real one." },
{ img: "img/print.webp", title: "Print", text: "The printer's plate, sizes in millimetres, checks and overhangs before you slice." },
{ img: "img/combine.webp", title: "Combine", text: "A cylinder taken out of a cube, copies in a row in a folder, a plane made thick." },
{ img: "img/crash.webp", title: "Even the crash screen", text: "If something does get burnt, your work comes back from the last autosave." },
],
formats: {
open: [".fry"],
save: [".fry (packed, with pictures)"],
export: ["GLB", "glTF", "FBX", "OBJ + MTL", "3MF", "STL", "PLY"],
note: "FBX opens in Blender, Unity, Unreal and Maya. 3MF and STL are in millimetres for slicers (PrusaSlicer, Bambu Studio, Cura, OrcaSlicer).",
},
keys: [
["Q W E R", "Select, move, turn, scale"],
["Tab", "Edit an object's shape"],
["D", "Draw a shape"],
["O", "Make a hole"],
["Ctrl+G", "Group into a folder"],
["Ctrl+Shift + / −", "Add together / take out"],
["Ctrl+1 … 5", "Model, Paint, Layout, Render, Print"],
["F1", "Every key, in the app"],
],
build: {
text: "Fry3DX builds from source on Linux with CMake. Everything else it needs is downloaded while building. On NixOS, shell.nix has the tools.",
steps: [
"git clone https://github.com/Nexoniarz/Fry3DX.git",
"cd Fry3DX",
"nix-shell --run 'cmake --preset release && cmake --build build/release'",
"build/release/bin/fry3dx",
],
needs: "Needs: a Vulkan graphics card, CMake, Ninja, a C++20 compiler, glslc (shaderc) and SDL3.",
},
versions: [
{ v: "0.4", date: "2026-10-19", items: ["Combine objects: add, subtract, overlap", "Draw shapes onto other objects", "Copies in a row or circle, thicken and hollow", "FBX export", "Folders and groups in Layers, go-to buttons", "Shift scales both sides, Shift+click loops and rings", "A crash screen of its own"] },
{ v: "0.3", date: "2026-10-19", items: ["Stickers that bend over edges, picture layers", "Packed .fry files, autosave and recovery", "Layout tab, Print tab, 3MF / STL / OBJ / PLY / GLB export", "Hole tool: hexagon nut pockets, rectangles, outlines"] },
{ v: "0.1", date: "2026-10-19", items: ["First version: shapes, shape editing, knife, painting, rendering"] },
],
};

BIN
docs/img/combine.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/img/crash.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

34
docs/img/logo.svg Normal file
View file

@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2a2a2f"/><stop offset="1" stop-color="#151518"/>
</linearGradient>
<linearGradient id="pan" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3b3b42"/><stop offset="1" stop-color="#1d1d21"/>
</linearGradient>
<linearGradient id="flame" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#ff4a1c"/><stop offset="1" stop-color="#ffc24a"/>
</linearGradient>
</defs>
<rect x="16" y="16" width="480" height="480" rx="96" fill="url(#bg)"/>
<!-- flames under the pan -->
<path d="M164 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
<path d="M236 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
<path d="M308 462 c -34 -14 -30 -60 0 -96 c 30 36 34 82 0 96 z" fill="url(#flame)"/>
<!-- handle -->
<path d="M372 300 L470 250" stroke="#ff7828" stroke-width="30" stroke-linecap="round"/>
<!-- the pan, seen a little from above -->
<ellipse cx="236" cy="318" rx="170" ry="72" fill="#ff7828"/>
<ellipse cx="236" cy="306" rx="150" ry="58" fill="url(#pan)"/>
<!-- the cube frying in it -->
<g stroke="#1a1a1d" stroke-width="8" stroke-linejoin="round">
<path d="M236 92 L330 140 L236 188 L142 140 Z" fill="#ffd2a0"/>
<path d="M142 140 L236 188 L236 300 L142 252 Z" fill="#ff8a3a"/>
<path d="M330 140 L236 188 L236 300 L330 252 Z" fill="#c9501a"/>
</g>
<!-- sizzle -->
<g stroke="#ffb070" stroke-width="10" stroke-linecap="round" fill="none" opacity="0.9">
<path d="M100 210 q-14 -18 0 -36 q14 -18 0 -36"/>
<path d="M378 200 q-14 -18 0 -36 q14 -18 0 -36"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
docs/img/model.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/img/paint.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/img/print.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

86
docs/index.html Normal file
View file

@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fry3DX</title>
<meta name="description" content="Fry3DX (Frying 3D Extreme): a 3D modeller that does what you'd expect. Made by Nexoniarz.">
<meta property="og:title" content="Fry3DX">
<meta property="og:description" content="A 3D modeller that does what you'd expect. Pick a tool, click, drag.">
<meta property="og:image" content="img/model.webp">
<link rel="icon" href="img/icon.png">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- The page is built from content.js by site.js: edit content.js to update it. -->
<header class="top">
<a class="brand" href="#"><img src="img/logo.svg" alt=""><span>Fry3DX</span></a>
<nav>
<a href="#features">Features</a>
<a href="#look">Look</a>
<a href="#get">Get it</a>
<a href="#versions">Versions</a>
<a class="gh" id="gh-top" href="#">GitHub</a>
</nav>
</header>
<main>
<section class="hero">
<div class="hero-text">
<p class="kicker" id="long"></p>
<h1 id="tagline"></h1>
<p class="lead" id="intro"></p>
<div class="buttons">
<a class="btn hot" href="#get">Get Fry3DX</a>
<a class="btn" id="gh-hero" href="#">Source on GitHub</a>
</div>
</div>
<figure class="hero-shot"><img src="img/model.webp" alt="Fry3DX with a table, chair, barrel and crate"></figure>
</section>
<section class="why" id="why"></section>
<section id="features">
<h2>What it does</h2>
<div class="grid" id="feature-grid"></div>
</section>
<section id="look">
<h2>How it looks</h2>
<div class="tabs" id="shot-tabs" role="tablist"></div>
<figure class="shot"><img id="shot-img" alt=""><figcaption id="shot-text"></figcaption></figure>
</section>
<section class="two">
<div>
<h2>Files</h2>
<div id="formats"></div>
</div>
<div>
<h2>Keys that make sense</h2>
<table class="keys" id="keys"></table>
</div>
</section>
<section id="get">
<h2>Get it</h2>
<p id="build-text"></p>
<div class="code"><button class="copy" id="copy">Copy</button><pre id="build-steps"></pre></div>
<p class="muted" id="build-needs"></p>
</section>
<section id="versions">
<h2>Versions</h2>
<div id="version-list"></div>
</section>
</main>
<footer>
<p>Fry3DX, made by <a id="author" href="#"></a>.</p>
<p class="muted">Sky pictures from Poly Haven (CC0).</p>
</footer>
<script src="content.js"></script>
<script src="site.js"></script>
</body>
</html>

91
docs/site.js Normal file
View file

@ -0,0 +1,91 @@
// Builds the page from window.FRY (content.js). Nothing here needs changing for a content update.
(function () {
const F = window.FRY;
const $ = (id) => document.getElementById(id);
const el = (tag, cls, text) => {
const e = document.createElement(tag);
if (cls) e.className = cls;
if (text != null) e.textContent = text;
return e;
};
$("long").textContent = F.long;
$("tagline").textContent = F.tagline;
$("intro").textContent = F.intro;
for (const id of ["gh-top", "gh-hero"]) $(id).href = F.repo;
$("author").textContent = F.author;
$("author").href = F.authorUrl;
const why = $("why");
why.append(el("h2", null, F.why.title));
for (const p of F.why.text) why.append(el("p", null, p));
const grid = $("feature-grid");
for (const f of F.features) {
const card = el("article", "card");
card.append(el("div", "icon", f.icon), el("h3", null, f.title), el("p", null, f.text));
grid.append(card);
}
// Screenshots: a tab each.
const tabs = $("shot-tabs");
const show = (i) => {
const s = F.shots[i];
$("shot-img").src = s.img;
$("shot-img").alt = s.title;
$("shot-text").textContent = s.text;
[...tabs.children].forEach((b, j) => b.setAttribute("aria-selected", i === j));
};
F.shots.forEach((s, i) => {
const b = el("button", "tab", s.title);
b.setAttribute("role", "tab");
b.onclick = () => show(i);
tabs.append(b);
new Image().src = s.img; // (loaded ahead, so switching is instant)
});
show(0);
const fm = $("formats");
const row = (label, items) => {
const d = el("div", "fmt");
d.append(el("span", "fmt-label", label));
for (const x of items) d.append(el("span", "chip", x));
fm.append(d);
};
row("Save", F.formats.save);
row("Export", F.formats.export);
fm.append(el("p", "muted", F.formats.note));
const keys = $("keys");
for (const [k, what] of F.keys) {
const tr = el("tr");
const td = el("td");
td.append(el("kbd", null, k));
tr.append(td, el("td", null, what));
keys.append(tr);
}
$("build-text").textContent = F.build.text;
$("build-steps").textContent = F.build.steps.join("\n");
$("build-needs").textContent = F.build.needs;
$("copy").onclick = async () => {
try {
await navigator.clipboard.writeText(F.build.steps.join("\n"));
$("copy").textContent = "Copied";
} catch {
$("copy").textContent = "Select and copy";
}
setTimeout(() => ($("copy").textContent = "Copy"), 1800);
};
const vl = $("version-list");
for (const v of F.versions) {
const d = el("div", "version");
const h = el("h3");
h.append(el("span", "v", "v" + v.v), el("span", "muted", " " + v.date));
const ul = el("ul");
for (const it of v.items) ul.append(el("li", null, it));
d.append(h, ul);
vl.append(d);
}
})();

103
docs/style.css Normal file
View file

@ -0,0 +1,103 @@
:root {
--bg: #161618;
--panel: #1f1f23;
--panel2: #26262b;
--line: #34343b;
--ink: #ecebe6;
--dim: #9c9a94;
--hot: #ff7828;
--hot2: #ffc24a;
--radius: 10px;
color-scheme: dark;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
a { color: var(--hot); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; display: block; }
h1, h2, h3 { line-height: 1.2; margin: 0 0 .6em; }
h2 { font-size: 1.7rem; }
h2::after { content: ""; display: block; width: 48px; height: 4px; margin-top: .4em; border-radius: 2px; background: linear-gradient(90deg, var(--hot), var(--hot2)); }
.muted { color: var(--dim); }
.top {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 10px max(16px, calc((100% - 1120px) / 2));
background: rgba(22, 22, 24, .9); backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); font-weight: 700; font-size: 1.15rem; }
.brand img { width: 32px; height: 32px; }
.brand:hover { text-decoration: none; }
nav { display: flex; gap: 18px; flex-wrap: wrap; justify-content: flex-end; }
nav a { color: var(--dim); font-size: .95rem; }
nav a:hover { color: var(--ink); text-decoration: none; }
nav .gh { color: var(--hot); }
main { max-width: 1120px; margin: 0 auto; padding: 0 16px; }
section { padding: 56px 0; border-bottom: 1px solid var(--line); }
section:last-child { border-bottom: 0; }
.hero { display: grid; grid-template-columns: 1fr 1.25fr; gap: 40px; align-items: center; padding-top: 64px; }
.kicker { color: var(--hot); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; font-size: .85rem; margin: 0 0 .6em; }
.hero h1 { font-size: clamp(2rem, 4.5vw, 3.2rem); }
.lead { color: var(--dim); font-size: 1.1rem; }
.buttons { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 24px; }
.btn { display: inline-block; padding: 12px 20px; border-radius: var(--radius); background: var(--panel2); color: var(--ink); font-weight: 600; border: 1px solid var(--line); }
.btn:hover { text-decoration: none; border-color: var(--hot); }
.btn.hot { background: linear-gradient(135deg, var(--hot), #ff4a1c); border-color: transparent; color: #fff; }
.hero-shot { margin: 0; }
.hero-shot img, .shot img { border-radius: var(--radius); border: 1px solid var(--line); box-shadow: 0 20px 60px rgba(0, 0, 0, .45); }
.why { max-width: 760px; }
.why p { font-size: 1.08rem; color: #cfcdc7; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; transition: border-color .15s, transform .15s; }
.card:hover { border-color: var(--hot); transform: translateY(-2px); }
.card .icon { font-size: 1.5rem; color: var(--hot); margin-bottom: 6px; }
.card h3 { font-size: 1.05rem; }
.card p { margin: 0; color: var(--dim); font-size: .95rem; }
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.tab { font: inherit; color: var(--dim); background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 6px 16px; cursor: pointer; }
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: #fff; background: var(--hot); border-color: var(--hot); }
.shot { margin: 0; }
.shot figcaption { color: var(--dim); margin-top: 12px; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
.fmt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.fmt-label { width: 64px; color: var(--dim); }
.chip { background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 2px 10px; font-size: .9rem; }
.keys { border-collapse: collapse; width: 100%; }
.keys td { padding: 7px 0; border-bottom: 1px solid var(--line); }
.keys td:first-child { width: 45%; }
kbd { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; }
.code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); }
.code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 ui-monospace, "DejaVu Sans Mono", monospace; color: #d6d4cd; }
.copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; }
.copy:hover { border-color: var(--hot); }
.version { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 22px; margin-bottom: 12px; }
.version h3 { margin-bottom: .3em; }
.version .v { color: var(--hot); }
.version ul { margin: 0; padding-left: 20px; color: #cfcdc7; }
footer { text-align: center; padding: 40px 16px; border-top: 1px solid var(--line); color: var(--dim); }
footer p { margin: 4px 0; }
@media (max-width: 820px) {
.hero, .two { grid-template-columns: 1fr; }
.hero { padding-top: 32px; gap: 28px; }
nav a:not(.gh) { display: none; }
section { padding: 40px 0; }
}