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:
1 parent
20786f4b5a
commit
97b3b5f685
40 files changed
+2553
-353
No files matched your search
@@ -0,0 +1,126 @@
|
||||
import { Box, Role, roleOf } from './svgModel';
|
||||
|
||||
/**
|
||||
* Turns parts of the live Archify SVG into images. Each element's computed
|
||||
* style is written inline into a standalone copy, so the image carries the
|
||||
* current theme, preset, focus and dimming exactly as the 2D view shows them.
|
||||
*/
|
||||
|
||||
/** Presentation properties copied from computed style. */
|
||||
const PROPS = [
|
||||
'fill', 'fill-opacity', 'fill-rule',
|
||||
'stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'stroke-dashoffset',
|
||||
'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit',
|
||||
// visibility is left out: the 3D view hides the 2D SVG with it.
|
||||
'opacity', 'display', 'filter', 'paint-order',
|
||||
'font-family', 'font-size', 'font-weight', 'font-style', 'letter-spacing', 'text-anchor',
|
||||
'dominant-baseline', 'text-transform',
|
||||
];
|
||||
|
||||
let fontCss: string | undefined;
|
||||
|
||||
/** The page's @font-face rules (Archify embeds JetBrains Mono as data URIs). */
|
||||
function fontFaces(): string {
|
||||
if (fontCss !== undefined) return fontCss;
|
||||
const rules: string[] = [];
|
||||
for (const sheet of [...document.styleSheets]) {
|
||||
let list: CSSRuleList;
|
||||
try {
|
||||
list = sheet.cssRules;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const rule of [...list]) if (rule instanceof CSSFontFaceRule) rules.push(rule.cssText);
|
||||
}
|
||||
fontCss = rules.join('\n');
|
||||
return fontCss;
|
||||
}
|
||||
|
||||
export interface LayerOptions {
|
||||
/** Roles whose elements are drawn; everything else is hidden. */
|
||||
keep: Role[];
|
||||
/** Draw kept nodes fully opaque; the 3D view applies their dimming itself. */
|
||||
opaqueNodes?: boolean;
|
||||
}
|
||||
|
||||
/** Copies computed styles from the live tree onto a clone with the same shape. */
|
||||
function inlineStyles(live: Element, copy: Element, options: LayerOptions, inherited: Role | undefined, root: boolean): void {
|
||||
const role = inherited ?? roleOf(live);
|
||||
const isDefs = live.tagName === 'defs' || !!live.closest('defs');
|
||||
const style = getComputedStyle(live);
|
||||
const parts: string[] = [];
|
||||
for (const prop of PROPS) {
|
||||
const value = style.getPropertyValue(prop);
|
||||
if (value) parts.push(`${prop}:${value}`);
|
||||
}
|
||||
let hidden = false;
|
||||
if (root) {
|
||||
// The root carries viewer state (camera transform, our fade); keep only
|
||||
// what its children inherit.
|
||||
copy.setAttribute('style', parts.filter((p) => /^(font|letter|text)/.test(p)).join(';'));
|
||||
} else {
|
||||
if (!isDefs) {
|
||||
if (role) hidden = !options.keep.includes(role);
|
||||
else if (live.children.length === 0) hidden = !options.keep.includes('floor');
|
||||
}
|
||||
if (options.opaqueNodes && role === 'node' && !inherited) parts.push('opacity:1');
|
||||
if (hidden) parts.push('display:none');
|
||||
copy.setAttribute('style', parts.join(';'));
|
||||
}
|
||||
copy.removeAttribute('class');
|
||||
if (hidden) return;
|
||||
const liveKids = live.children;
|
||||
const copyKids = copy.children;
|
||||
for (let i = 0; i < liveKids.length && i < copyKids.length; i++) {
|
||||
inlineStyles(liveKids[i], copyKids[i], options, role, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standalone SVG markup for one layer of the diagram, sized at `scale`
|
||||
* pixels per user unit so the browser rasterizes it sharply.
|
||||
*/
|
||||
export function layerMarkup(svg: SVGSVGElement, viewBox: Box, options: LayerOptions, scale: number): string {
|
||||
const copy = svg.cloneNode(true) as SVGSVGElement;
|
||||
inlineStyles(svg, copy, options, undefined, true);
|
||||
// Viewer-only children (focus overlays, hit areas) carry no data roles but
|
||||
// are not part of the authored drawing.
|
||||
copy.querySelectorAll('[data-viewer-only], .no-export').forEach((el) => el.remove());
|
||||
copy.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
copy.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
|
||||
copy.setAttribute('width', String(Math.round(viewBox.w * scale)));
|
||||
copy.setAttribute('height', String(Math.round(viewBox.h * scale)));
|
||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||
style.textContent = fontFaces();
|
||||
copy.insertBefore(style, copy.firstChild);
|
||||
return new XMLSerializer().serializeToString(copy);
|
||||
}
|
||||
|
||||
export interface Raster {
|
||||
canvas: HTMLCanvasElement;
|
||||
/** Pixels per SVG user unit. */
|
||||
scale: number;
|
||||
/** SVG area the canvas covers (the view box plus a margin). */
|
||||
area: Box;
|
||||
}
|
||||
|
||||
/** Rasterizes SVG markup over `area` (in user units) at `scale` pixels per unit. */
|
||||
export async function rasterize(markup: string, viewBox: Box, area: Box, scale: number, paint?: (ctx: CanvasRenderingContext2D) => void): Promise<Raster> {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(area.w * scale));
|
||||
canvas.height = Math.max(1, Math.round(area.h * scale));
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.scale(scale, scale);
|
||||
ctx.translate(-area.x, -area.y);
|
||||
paint?.(ctx);
|
||||
const url = URL.createObjectURL(new Blob([markup], { type: 'image/svg+xml' }));
|
||||
try {
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
await image.decode();
|
||||
ctx.drawImage(image, viewBox.x, viewBox.y, viewBox.w, viewBox.h);
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
return { canvas, scale, area };
|
||||
}
|
||||
Reference in new issue
Block a user