Add a 3D view of every diagram; organise source by job; 0.6.0

3D view: a "3D" menu beside Export switches the diagram to a three.js
scene built from the live Archify SVG. Nodes become blocks topped with
the node as Archify draws it, relationships become lit pipes along
Archify's routes, and the canvas, frames, lanes and legend form the
floor. Clicks and hover go to the SVG nodes, so focus, the Semantic
Passport and the intent trace work as in 2D; Archify's own animations
(trace, node pulse, passport/path/lens flows) are mirrored live. The
menu also offers flat or stacked levels, reset view and a top view;
Export gains "3D view" (PNG). Trackpad scrolling pans, pinch and the
mouse wheel zoom, and the zoom buttons work beyond the 2D limits.
The scene draws only when something moves. New setting
archify.defaultView.

Source: src/ is split by job (commands, views, webview, archify,
sources, language, util, viewer3d); behaviour unchanged.
This commit is contained in:
root-at-skic committed 2026-10-11 00:54:00 +03:00
1 parent 20786f4b5a
commit 97b3b5f685
40 files changed
+2553 -353

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
import path from 'node:path';
import * as vscode from 'vscode';
import { repoRootFor } from '../archify/runtime';
import { log } from '../util/log';
export type WebviewMessage =
| { type: 'hash'; hash: string }
| { type: 'save'; filename: string; base64: string }
| { type: 'openExternal'; href: string }
| { type: 'openRelative'; href: string }
| { type: 'openSource'; path: string; line?: number; endLine?: number; href?: string }
| { type: 'error'; message: string };
/** Column for opening files next to a webview without covering it. */
function besideColumn(panel: vscode.WebviewPanel): vscode.ViewColumn {
return panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One;
}
async function exists(uri: vscode.Uri): Promise<boolean> {
try {
await vscode.workspace.fs.stat(uri);
return true;
} catch {
return false;
}
}
/**
* Opens a diagram's source evidence in the editor. The path is relative to
* the repository root; the diagram's checkout and the workspace folders are
* tried in turn before falling back to the web link.
*/
async function openSource(
message: Extract<WebviewMessage, { type: 'openSource' }>,
context: vscode.Uri,
panel: vscode.WebviewPanel,
): Promise<void> {
const relative = message.path.replace(/^\/+/, '');
const roots = new Set<string>();
const repoRoot = await repoRootFor(context);
if (repoRoot) roots.add(repoRoot);
for (const folder of vscode.workspace.workspaceFolders ?? []) roots.add(folder.uri.fsPath);
for (const root of roots) {
const candidate = vscode.Uri.file(path.join(root, relative));
if (relative && (await exists(candidate))) {
const start = Math.max((message.line ?? 1) - 1, 0);
const end = Math.max((message.endLine ?? message.line ?? 1) - 1, start);
const selection = new vscode.Range(start, 0, end, Number.MAX_SAFE_INTEGER);
await vscode.window.showTextDocument(candidate, { viewColumn: besideColumn(panel), selection });
return;
}
}
if (message.href && /^https?:/i.test(message.href)) {
await vscode.env.openExternal(vscode.Uri.parse(message.href));
return;
}
vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`);
}
export async function handleWebviewMessage(
message: WebviewMessage,
context: vscode.Uri,
panel: vscode.WebviewPanel,
): Promise<void> {
try {
switch (message.type) {
case 'openExternal':
await vscode.env.openExternal(vscode.Uri.parse(message.href));
break;
case 'openRelative': {
const [file] = message.href.split('#');
if (!file) break;
const target = vscode.Uri.joinPath(context, '..', decodeURIComponent(file));
await vscode.commands.executeCommand('vscode.open', target, besideColumn(panel));
break;
}
case 'openSource':
await openSource(message, context, panel);
break;
case 'save': {
const name = path.basename(message.filename || 'archify-export');
const defaultUri = vscode.Uri.joinPath(context, '..', name);
const target = await vscode.window.showSaveDialog({ defaultUri, saveLabel: 'Export' });
if (!target) break;
await vscode.workspace.fs.writeFile(target, Buffer.from(message.base64, 'base64'));
const choice = await vscode.window.showInformationMessage(`Exported ${path.basename(target.fsPath)}`, 'Reveal');
if (choice === 'Reveal') await vscode.commands.executeCommand('revealFileInOS', target);
break;
}
case 'error':
vscode.window.showErrorMessage(`Archify: ${message.message}`);
break;
case 'hash':
break;
}
} catch (error) {
log().error(error instanceof Error ? error : String(error));
vscode.window.showErrorMessage(`Archify: ${error instanceof Error ? error.message : String(error)}`);
}
}