Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas, login rate limiting, audit log and backups - Ghidra headless decompilation (ARM64 natives built from source), binary_info tool, clean room / deep workflow per session - Recreate skill and commands for analysing and re-building old software - File checkpoints with rewind/restore, background shells (bash_output, kill_shell) - web_search tool (DuckDuckGo, SearXNG, Brave, Tavily) - In-app dialogs and toasts instead of browser dialogs - 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One, Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fab9add5d5
commit
5b569b6835
108 files changed
+6785
-279
No files matched your search
@@ -0,0 +1,153 @@
|
||||
// 3D preview for GLB/GLTF, STL, OBJ and STEP/IGES (via OpenCascade WASM).
|
||||
// Loaded on demand only when such a file is opened.
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
|
||||
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
||||
import occtScriptUrl from "occt-import-js/dist/occt-import-js.js?url";
|
||||
import occtWasmUrl from "occt-import-js/dist/occt-import-js.wasm?url";
|
||||
|
||||
let occtPromise: Promise<any> | undefined;
|
||||
|
||||
/** Load the OpenCascade importer once (script tag + wasm URL). */
|
||||
function loadOcct(): Promise<any> {
|
||||
occtPromise ??= new Promise((resolve, reject) => {
|
||||
const s = document.createElement("script");
|
||||
s.src = occtScriptUrl;
|
||||
s.onload = () => (window as any).occtimportjs({ locateFile: () => occtWasmUrl }).then(resolve, reject);
|
||||
s.onerror = () => reject(new Error("could not load the STEP importer"));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return occtPromise;
|
||||
}
|
||||
|
||||
function cssColor(name: string, fallback: string) {
|
||||
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return v || fallback;
|
||||
}
|
||||
|
||||
async function loadObject(url: string, ext: string): Promise<THREE.Object3D> {
|
||||
const material = new THREE.MeshStandardMaterial({ color: cssColor("--blue", "#83a598"), metalness: 0.1, roughness: 0.6, side: THREE.DoubleSide });
|
||||
switch (ext) {
|
||||
case "glb":
|
||||
case "gltf":
|
||||
return (await new GLTFLoader().loadAsync(url)).scene;
|
||||
case "stl": {
|
||||
const geo = await new STLLoader().loadAsync(url);
|
||||
geo.computeVertexNormals();
|
||||
return new THREE.Mesh(geo, material);
|
||||
}
|
||||
case "obj": {
|
||||
const obj = await new OBJLoader().loadAsync(url);
|
||||
obj.traverse((c) => c instanceof THREE.Mesh && (c.material = material));
|
||||
return obj;
|
||||
}
|
||||
default: {
|
||||
// STEP / IGES: tessellate with OpenCascade in the browser.
|
||||
const [occt, buf] = await Promise.all([loadOcct(), fetch(url, { credentials: "same-origin" }).then((r) => r.arrayBuffer())]);
|
||||
const data = new Uint8Array(buf);
|
||||
const res = ext === "igs" || ext === "iges" ? occt.ReadIgesFile(data, null) : occt.ReadStepFile(data, null);
|
||||
if (!res.success) throw new Error("the file could not be read");
|
||||
const group = new THREE.Group();
|
||||
for (const m of res.meshes) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(m.attributes.position.array, 3));
|
||||
if (m.attributes.normal) geo.setAttribute("normal", new THREE.Float32BufferAttribute(m.attributes.normal.array, 3));
|
||||
else geo.computeVertexNormals();
|
||||
if (m.index) geo.setIndex(new THREE.Uint32BufferAttribute(m.index.array, 1));
|
||||
const mat = m.color ? material.clone() : material;
|
||||
if (m.color) (mat as THREE.MeshStandardMaterial).color = new THREE.Color(m.color[0], m.color[1], m.color[2]);
|
||||
group.add(new THREE.Mesh(geo, mat));
|
||||
}
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ModelViewer({ url, ext }: { url: string; ext: string }) {
|
||||
const host = useRef<HTMLDivElement>(null);
|
||||
const [state, setState] = useState<{ loading: boolean; error?: string; info?: string }>({ loading: true });
|
||||
|
||||
useEffect(() => {
|
||||
const el = host.current!;
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
el.appendChild(renderer.domElement);
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(cssColor("--bg0", "#282828"));
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.6));
|
||||
const sun = new THREE.DirectionalLight(0xffffff, 1.8);
|
||||
sun.position.set(3, 5, 4);
|
||||
scene.add(sun);
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 1e6);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
const resize = () => {
|
||||
const w = el.clientWidth || 300;
|
||||
const h = el.clientHeight || 300;
|
||||
renderer.setSize(w, h);
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(el);
|
||||
resize();
|
||||
|
||||
let alive = true;
|
||||
let frame = 0;
|
||||
const loop = () => {
|
||||
if (!alive) return;
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
frame = requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
|
||||
loadObject(url, ext)
|
||||
.then((obj) => {
|
||||
if (!alive) return;
|
||||
scene.add(obj);
|
||||
// Frame the model.
|
||||
const box = new THREE.Box3().setFromObject(obj);
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const radius = Math.max(size.length() / 2, 1e-3);
|
||||
camera.position.copy(center).add(new THREE.Vector3(radius * 1.4, radius * 1.0, radius * 1.8));
|
||||
camera.near = radius / 100;
|
||||
camera.far = radius * 100;
|
||||
camera.updateProjectionMatrix();
|
||||
controls.target.copy(center);
|
||||
let tris = 0;
|
||||
obj.traverse((c) => {
|
||||
if (c instanceof THREE.Mesh) tris += (c.geometry.index ? c.geometry.index.count : c.geometry.attributes.position.count) / 3;
|
||||
});
|
||||
setState({ loading: false, info: `${Math.round(tris).toLocaleString()} triangles · ${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}` });
|
||||
})
|
||||
.catch((e) => alive && setState({ loading: false, error: String(e?.message ?? e) }));
|
||||
|
||||
return () => {
|
||||
alive = false;
|
||||
cancelAnimationFrame(frame);
|
||||
ro.disconnect();
|
||||
controls.dispose();
|
||||
scene.traverse((c) => {
|
||||
if (c instanceof THREE.Mesh) {
|
||||
c.geometry.dispose();
|
||||
(Array.isArray(c.material) ? c.material : [c.material]).forEach((m) => m.dispose());
|
||||
}
|
||||
});
|
||||
renderer.dispose();
|
||||
el.removeChild(renderer.domElement);
|
||||
};
|
||||
}, [url, ext]);
|
||||
|
||||
return (
|
||||
<div className="model-viewer">
|
||||
<div ref={host} className="model-canvas" />
|
||||
<div className="model-info">{state.loading ? "…" : state.error ? `⚠ ${state.error}` : state.info}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user