feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
+42
View File
@@ -0,0 +1,42 @@
# SimpleCAD Scene Viewer
This is a browser-only viewer for exported `Scene Schema 1.0` packages. It
does not import SimpleCAD, Python, or OpenCascade. The loader reads the
canonical `.scene.zip`, parses `scene.json`, and loads the referenced GLB and
entity sidecar assets. When present, it also loads `model/model.json` and the
manifest-declared Python files under `sources/`. ZIP resource limits and every
referenced member's exact byte length and SHA-256 are checked before scene
assets are parsed.
## Run
```bash
npm install
npm run prepare:example
npm run dev
```
Open `http://localhost:5173/`, click **Open .scene.zip**, and select the package
generated at `examples/out/hydraulic_rod_assembly/hydraulic_rod_assembly.scene.zip`.
The same file can be dropped directly into the viewport.
The file picker accepts any local `.scene.zip` package. The viewer does not
load scenes from URL query parameters or require a built-in case registry.
## Inspecting A Model
The left **ASSEMBLY** tab shows the evaluated occurrence hierarchy. Click an
occurrence to inspect its definition, visibility, and evaluated body data. Use
the visibility control on a row to hide or show that occurrence.
The **FEATURES** tab shows the embedded replayable operation tree from
`model/model.json`. Select an operation to inspect its canonical operation name,
category, inputs, output count, parameters, and summary. For mapped operations,
the inspector shows assignment targets and the complete embedded Python file,
scrolls to the originating call, and highlights its source line range.
Click a rendered face or CAD edge in the viewport to select the corresponding
evaluated entity. The selected face or edge is highlighted and its measured
properties are shown in the inspector. The scene package must contain the
embedded model artifact for the Features tab; older scene packages still retain
assembly-tree and geometry selection support.
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0b0e12" />
<title>SimpleCAD Scene Viewer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "simplecad-scene-viewer",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"prepare:example": "uv run python ../examples/10_part_assembly.py"
},
"dependencies": {
"fflate": "0.8.2",
"lucide": "^0.547.0",
"three": "0.178.0"
},
"devDependencies": {
"@types/three": "0.178.1",
"typescript": "5.7.2",
"vite": "7.3.6"
}
}
@@ -0,0 +1,68 @@
import * as THREE from 'three';
import type { OrbitControls } from 'three/addons/controls/OrbitControls.js';
type ClickSelectionOptions = {
element: HTMLElement;
camera: THREE.PerspectiveCamera;
controls: OrbitControls;
movementThreshold?: number;
onPointerStateChange?: (active: boolean) => void;
onClick: (event: PointerEvent) => void;
};
export function bindClickSelection(options: ClickSelectionOptions): () => void {
const { element, camera, controls, onClick, movementThreshold = 4 } = options;
let pointerDown: {
x: number;
y: number;
button: number;
position: THREE.Vector3;
target: THREE.Vector3;
zoom: number;
} | null = null;
const onPointerDown = (event: PointerEvent): void => {
pointerDown = {
x: event.clientX,
y: event.clientY,
button: event.button,
position: camera.position.clone(),
target: controls.target.clone(),
zoom: camera.zoom,
};
options.onPointerStateChange?.(true);
};
const onPointerUp = (event: PointerEvent): void => {
const down = pointerDown;
pointerDown = null;
options.onPointerStateChange?.(false);
if (!down || down.button !== 0 || event.button !== 0) return;
if (Math.hypot(event.clientX - down.x, event.clientY - down.y) > movementThreshold) return;
// OrbitControls treats any pointer movement as rotation. Drain that movement,
// then restore the click-time camera before committing selection.
const damping = controls.enableDamping;
controls.enableDamping = false;
controls.update();
camera.position.copy(down.position);
camera.zoom = down.zoom;
camera.updateProjectionMatrix();
controls.target.copy(down.target);
controls.update();
controls.enableDamping = damping;
onClick(event);
};
const onPointerLeave = (): void => {
pointerDown = null;
options.onPointerStateChange?.(false);
};
element.addEventListener('pointerdown', onPointerDown);
element.addEventListener('pointerup', onPointerUp);
element.addEventListener('pointerleave', onPointerLeave);
return () => {
element.removeEventListener('pointerdown', onPointerDown);
element.removeEventListener('pointerup', onPointerUp);
element.removeEventListener('pointerleave', onPointerLeave);
};
}
@@ -0,0 +1,110 @@
type PanelSide = 'navigator' | 'inspector';
type ResizablePanelsOptions = {
workspace: HTMLElement;
navigatorPanel: HTMLElement;
inspectorPanel: HTMLElement;
navigatorResizer: HTMLElement;
inspectorResizer: HTMLElement;
minNavigatorWidth?: number;
maxNavigatorWidth?: number;
minInspectorWidth?: number;
maxInspectorWidth?: number;
minCenterWidth?: number;
gutterWidth?: number;
desktopMedia?: string;
};
export function bindResizablePanels(options: ResizablePanelsOptions): () => void {
const {
workspace,
navigatorPanel,
inspectorPanel,
navigatorResizer,
inspectorResizer,
minNavigatorWidth = 180,
maxNavigatorWidth = 520,
minInspectorWidth = 230,
maxInspectorWidth = 620,
minCenterWidth = 320,
gutterWidth = 14,
desktopMedia = '(min-width: 901px)',
} = options;
const media = window.matchMedia(desktopMedia);
const cleanups: Array<() => void> = [];
const panelFor = (side: PanelSide): HTMLElement => side === 'navigator' ? navigatorPanel : inspectorPanel;
const otherPanelFor = (side: PanelSide): HTMLElement => side === 'navigator' ? inspectorPanel : navigatorPanel;
const resizerFor = (side: PanelSide): HTMLElement => side === 'navigator' ? navigatorResizer : inspectorResizer;
const setWidth = (side: PanelSide, requestedWidth: number): void => {
if (!media.matches) return;
const minimum = side === 'navigator' ? minNavigatorWidth : minInspectorWidth;
const configuredMaximum = side === 'navigator' ? maxNavigatorWidth : maxInspectorWidth;
const available = workspace.getBoundingClientRect().width
- otherPanelFor(side).getBoundingClientRect().width
- minCenterWidth
- gutterWidth;
const maximum = Math.max(minimum, Math.min(configuredMaximum, available));
const width = Math.round(Math.max(minimum, Math.min(maximum, requestedWidth)));
workspace.style.setProperty(side === 'navigator' ? '--navigator-width' : '--inspector-width', `${width}px`);
const resizer = resizerFor(side);
resizer.setAttribute('aria-valuemin', String(minimum));
resizer.setAttribute('aria-valuemax', String(maximum));
resizer.setAttribute('aria-valuenow', String(width));
};
const bind = (side: PanelSide): void => {
const resizer = resizerFor(side);
const onPointerDown = (event: PointerEvent): void => {
if (event.button !== 0 || !media.matches) return;
event.preventDefault();
const startX = event.clientX;
const startWidth = panelFor(side).getBoundingClientRect().width;
resizer.setPointerCapture(event.pointerId);
resizer.classList.add('dragging');
document.body.classList.add('resizing-panels');
const onPointerMove = (moveEvent: PointerEvent): void => {
const delta = moveEvent.clientX - startX;
setWidth(side, startWidth + (side === 'navigator' ? delta : -delta));
};
const finish = (): void => {
resizer.classList.remove('dragging');
document.body.classList.remove('resizing-panels');
resizer.removeEventListener('pointermove', onPointerMove);
resizer.removeEventListener('pointerup', finish);
resizer.removeEventListener('pointercancel', finish);
};
resizer.addEventListener('pointermove', onPointerMove);
resizer.addEventListener('pointerup', finish);
resizer.addEventListener('pointercancel', finish);
};
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
event.preventDefault();
const direction = event.key === 'ArrowRight' ? 1 : -1;
const currentWidth = panelFor(side).getBoundingClientRect().width;
setWidth(side, currentWidth + direction * (side === 'navigator' ? 12 : -12));
};
resizer.addEventListener('pointerdown', onPointerDown);
resizer.addEventListener('keydown', onKeyDown);
cleanups.push(() => resizer.removeEventListener('pointerdown', onPointerDown));
cleanups.push(() => resizer.removeEventListener('keydown', onKeyDown));
};
bind('navigator');
bind('inspector');
setWidth('navigator', navigatorPanel.getBoundingClientRect().width);
setWidth('inspector', inspectorPanel.getBoundingClientRect().width);
const resizeObserver = new ResizeObserver(() => {
if (!media.matches) return;
setWidth('navigator', navigatorPanel.getBoundingClientRect().width);
setWidth('inspector', inspectorPanel.getBoundingClientRect().width);
});
resizeObserver.observe(workspace);
cleanups.push(() => resizeObserver.disconnect());
return () => cleanups.forEach((cleanup) => cleanup());
}
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
:root { color: #e7edf5; background: #0b0e12; font-family: 'Space Grotesk', system-ui, sans-serif; font-synthesis: none; }
* { box-sizing: border-box; }
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { display: none; width: 0; height: 0; }
body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; background: #0b0e12; }
body.resizing-panels { cursor: col-resize; user-select: none; }
body.resizing-dock { cursor: row-resize; user-select: none; }
button, select { font: inherit; }
.ui-icon, .button-icon { display: block; flex: 0 0 auto; width: 13px; height: 13px; }
.shell { display: grid; grid-template-rows: 64px minmax(0, 1fr) 32px; height: 100vh; }
.topbar { display: flex; align-items: center; justify-content: space-between; padding: 0 24px; border-bottom: 1px solid #202832; background: #10141a; }
.brand, .top-actions, .brand > div { display: flex; align-items: center; }
.brand { gap: 10px; letter-spacing: -.02em; }
.brand-mark { display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid #b3e36b; color: #b3e36b; font: 500 11px 'DM Mono', monospace; transform: rotate(-7deg); }
.brand strong { display: block; font-size: 14px; line-height: 15px; }
.brand span:not(.brand-mark) { display: block; color: #758397; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
.top-actions { gap: 18px; }
.case-picker { display: flex; align-items: center; gap: 9px; color: #738093; font: 500 10px 'DM Mono', monospace; }
.case-picker select { max-width: 230px; padding: 7px 25px 7px 9px; border: 1px solid #2a3440; border-radius: 3px; color: #dce5ee; background: #151b22; font: 12px 'DM Mono', monospace; }
.quiet-button, .viewport-tools button { border: 1px solid #303b48; border-radius: 3px; color: #aebdcd; background: #151b22; cursor: pointer; font: 500 10px 'DM Mono', monospace; letter-spacing: .04em; }
.quiet-button { display: inline-flex; align-items: center; gap: 7px; padding: 8px 11px; }
.quiet-button:hover, .viewport-tools button:hover { border-color: #b3e36b; color: #b3e36b; }
.workspace { --navigator-width: 256px; --inspector-width: 278px; display: grid; grid-template-columns: var(--navigator-width) 7px minmax(0, 1fr) 7px var(--inspector-width); min-width: 0; min-height: 0; overflow: hidden; }
.panel { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; background: #10141a; }
.panel > .panel-heading { flex: 0 0 auto; }
.tree-panel { grid-column: 1; }
.viewport-wrap { grid-column: 3; }
.details-panel { grid-column: 5; }
.panel-resizer { position: relative; z-index: 4; min-width: 0; cursor: col-resize; touch-action: none; background: #10141a; }
.panel-resizer::before { position: absolute; top: 0; bottom: 0; left: 3px; width: 1px; content: ''; background: #202832; transition: background .15s, box-shadow .15s; }
.panel-resizer:hover::before, .panel-resizer.dragging::before, .panel-resizer:focus-visible::before { background: #b3e36b; box-shadow: 0 0 8px rgba(179,227,107,.5); }
#navigator-resizer { grid-column: 2; }
#inspector-resizer { grid-column: 4; }
.panel-heading { display: flex; align-items: flex-start; justify-content: space-between; min-height: 80px; padding: 21px 17px 15px; border-bottom: 1px solid #202832; }
.eyebrow { display: block; color: #718096; font: 500 9px 'DM Mono', monospace; letter-spacing: .15em; }
h1 { margin: 7px 0 0; color: #eef4fb; font-size: 17px; font-weight: 600; letter-spacing: -.04em; text-transform: capitalize; }
.count { min-width: 27px; padding: 4px 6px; border: 1px solid #2b3540; border-radius: 3px; color: #8190a3; text-align: center; font: 10px 'DM Mono', monospace; }
.tree { min-height: 0; flex: 1 1 auto; padding: 12px 0; overflow: auto; overscroll-behavior: contain; }
.tree-empty { padding: 24px 16px; color: #718094; font: 10px/1.6 'DM Mono', monospace; }
.tree-row { display: flex; align-items: center; gap: 7px; width: 100%; padding: 8px 12px 8px calc(12px + var(--depth) * 16px); border: 0; border-left: 2px solid transparent; color: #94a1b2; background: transparent; text-align: left; cursor: pointer; }
.tree-row:hover { color: #edf3f9; background: #171d24; }
.tree-row.selected { border-left-color: #b3e36b; color: #f2f8fb; background: #1a2324; }
.tree-row.hidden-node { color: #4d5a68; }.tree-row.hidden-node .tree-visibility { color: #b3e36b; opacity: .55; }
.tree-chevron { display: grid; flex: 0 0 10px; place-items: center; width: 10px; color: #697889; }
.tree-chevron-icon { display: block; width: 10px; height: 10px; }
.tree-glyph { display: grid; flex: 0 0 14px; place-items: center; width: 14px; color: #b3e36b; }
.tree-type-icon { display: block; width: 14px; height: 14px; }
.tree-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.tree-visibility { display: grid; flex: 0 0 18px; place-items: center; margin-left: auto; color: #43505e; }
.tree-visibility-icon { display: block; width: 14px; height: 14px; }
.navigator-tabs { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid #202832; background: #0d1218; }
.navigator-tab { display: flex; align-items: center; justify-content: center; gap: 6px; min-width: 0; padding: 10px 8px; border: 0; border-bottom: 2px solid transparent; color: #718094; background: transparent; cursor: pointer; font: 10px 'DM Mono', monospace; text-transform: uppercase; }
.navigator-tab:hover { color: #dce5ee; background: #151b22; }
.navigator-tab.active { border-bottom-color: #b3e36b; color: #b3e36b; background: #151d1b; }
.navigator-tab .count { min-width: 22px; padding: 3px 5px; font-size: 9px; }
.tab-icon { display: flex; flex: 0 0 auto; }
.tab-icon svg { display: block; width: 13px; height: 13px; }
.navigator-view { display: flex; min-width: 0; min-height: 0; flex: 1 1 auto; flex-direction: column; overflow: hidden; }
.navigator-view[hidden] { display: none; }
.feature-tree { padding-top: 12px; }
.feature-tree-row { padding-right: 10px; }
.feature-tree-children { display: block; }
.feature-glyph { color: #91b8ed; }
.feature-tree-row.selected .feature-glyph { color: #b3e36b; }
.feature-tag-mark { margin-left: auto; color: #718094; font: 8px 'DM Mono', monospace; letter-spacing: .06em; }
.viewport-wrap { display: block; position: relative; min-width: 0; min-height: 0; overflow: hidden; background: radial-gradient(circle at 52% 44%, #18222a 0, #0d1218 47%, #0b0e12 100%); }
.viewport-stage { position: absolute; inset: 0; min-width: 0; min-height: 0; overflow: hidden; }
.viewport { position: absolute; inset: 0; overflow: hidden; }
.viewport.drop-target { outline: 2px solid #b3e36b; outline-offset: -8px; }
.viewport canvas { display: block; width: 100%; height: 100%; }
.viewport::after { position: absolute; inset: 0; pointer-events: none; content: ''; background: linear-gradient(90deg, rgba(179,227,107,.04) 1px, transparent 1px), linear-gradient(rgba(179,227,107,.04) 1px, transparent 1px); background-size: 64px 64px; mask-image: linear-gradient(to bottom, transparent 0, rgba(0,0,0,.45) 70%, transparent 100%); }
.hud { position: absolute; z-index: 2; left: 17px; bottom: 16px; display: flex; align-items: center; gap: 7px; padding: 7px 9px; border: 1px solid #27323d; border-radius: 3px; color: #8898aa; background: rgba(14, 19, 24, .8); font: 10px 'DM Mono', monospace; backdrop-filter: blur(8px); }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #697583; }.status-dot.ready { background: #b3e36b; box-shadow: 0 0 10px #b3e36b; }
.viewport-tools { position: absolute; z-index: 2; top: 15px; right: 15px; display: flex; align-items: stretch; gap: 7px; }
.viewport-tools button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 7px 9px; font-size: 9px; }
.viewport-tools button.active { border-color: #526d40; color: #b3e36b; background: #18221c; }
.selection-tools { display: flex; align-items: center; padding-left: 8px; border: 1px solid #303b48; border-radius: 3px; background: rgba(21, 27, 34, .92); }
.selection-tools .tool-label { margin-right: 4px; color: #657386; font: 8px 'DM Mono', monospace; letter-spacing: .08em; }
.selection-tools .selection-mode { border-width: 0 0 0 1px; border-radius: 0; background: transparent; }
.selection-tools .selection-mode:first-of-type { border-left: 0; }
.loading { position: absolute; z-index: 3; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 13px; color: #94a5b7; font: 11px 'DM Mono', monospace; background: rgba(11,14,18,.75); transition: opacity .2s; }.loading.hidden { opacity: 0; pointer-events: none; }
.spinner { width: 18px; height: 18px; border: 1px solid #364352; border-top-color: #b3e36b; border-radius: 50%; animation: spin .7s linear infinite; }@keyframes spin { to { transform: rotate(360deg); } }
.tag { padding: 4px 6px; border: 1px solid #39484d; border-radius: 3px; color: #b3e36b; background: #18221c; font: 9px 'DM Mono', monospace; }
.details { min-width: 0; min-height: 0; flex: 1 1 auto; overflow: auto; overscroll-behavior: contain; padding: 20px 17px; font-size: 13px; }.empty-state { display: flex; flex-direction: column; gap: 8px; padding: 34px 4px; color: #718094; font-size: 13px; line-height: 1.55; }.empty-state strong { color: #bbc7d4; font-size: 15px; }.empty-cross { display: block; width: 28px; height: 28px; color: #536475; }.empty-state-icon { display: block; width: 28px; height: 28px; }
.detail-title { display: flex; gap: 11px; min-width: 0; padding-bottom: 19px; border-bottom: 1px solid #202832; }.detail-icon { display: block; flex: 0 0 auto; width: 24px; color: #b3e36b; }.detail-icon-svg { display: block; width: 24px; height: 24px; }.detail-title > div { min-width: 0; }.detail-title > div > strong, .detail-title > div > span { display: block; }.detail-title strong { margin: 1px 0 6px; overflow-wrap: anywhere; color: #edf3f9; font-size: 16px; line-height: 1.35; }.detail-title > div > span { color: #718096; font: 11px/1.5 'DM Mono', monospace; }.detail-definition-path { overflow-wrap: anywhere; word-break: break-word; }
.detail-section { padding: 19px 0 4px; }.detail-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; }.detail-section dl { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr); gap: 10px 12px; margin: 13px 0 0; font: 11px/1.45 'DM Mono', monospace; }.detail-section dt { color: #657386; }.detail-section dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: #c5d0dc; text-align: right; word-break: break-word; }.path-value { white-space: normal; }.detail-muted, .selector-warning { color: #718094; font: 11px/1.6 'DM Mono', monospace; }.selector-warning { color: #c19a62; }
.params { max-height: 260px; margin: 12px 0 0; padding: 10px; overflow: auto; border: 1px solid #202832; color: #a9c77b; background: #0b0e12; font: 9px/1.5 'DM Mono', monospace; white-space: pre-wrap; word-break: break-word; }
.source-file-path { margin-top: 12px; overflow-wrap: anywhere; color: #91b8ed; font: 9px/1.5 'DM Mono', monospace; }
.source-code { max-height: 360px; margin: 8px 0 0; padding: 8px 0; overflow: auto; border: 1px solid #202832; color: #b8c4d1; background: #090c10; font: 10px/1.55 'DM Mono', monospace; tab-size: 4; }
.source-code code { display: block; min-width: max-content; }
.source-line { display: grid; grid-template-columns: 42px minmax(max-content, 1fr); min-height: 16px; }
.source-line.active { color: #f2f7ee; background: rgba(179, 227, 107, .13); box-shadow: inset 2px 0 #b3e36b; }
.source-line-number { padding-right: 10px; color: #465363; text-align: right; user-select: none; }
.source-line.active .source-line-number { color: #9abf66; }
.source-line-text { padding-right: 12px; white-space: pre; }
.selector-code { max-height: 240px; margin: 12px 0 0; padding: 10px; overflow: auto; border: 1px solid #39484d; color: #b3e36b; background: #101a16; font: 10px/1.55 'DM Mono', monospace; white-space: pre-wrap; word-break: break-word; }.copy-button { display: inline-flex; align-items: center; gap: 5px; padding: 5px 7px; border: 1px solid #39484d; border-radius: 3px; color: #b3e36b; background: #18221c; cursor: pointer; font: 9px 'DM Mono', monospace; letter-spacing: .08em; }.copy-button:hover { border-color: #b3e36b; }
.tag-list { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 11px; }.tag-list span { padding: 4px 6px; border: 1px solid #39484d; border-radius: 3px; color: #b3e36b; background: #18221c; font: 8px 'DM Mono', monospace; }
.detail-summary { color: #aebdcd; font: 12px/1.6 'DM Mono', monospace; }
.footer { display: flex; justify-content: space-between; align-items: center; padding: 0 17px; border-top: 1px solid #202832; color: #657487; background: #10141a; font: 9px 'DM Mono', monospace; }.footer-note { color: #4d5a68; }
@media (max-width: 900px) { body { overflow: auto; }.shell { height: auto; min-height: 100vh; grid-template-rows: 64px auto 32px; }.topbar { padding: 0 14px; }.case-picker { display: none; }.workspace { grid-template-columns: 210px minmax(0, 1fr); grid-template-rows: minmax(620px, 78vh) auto; overflow: visible; }.panel-resizer { display: none; }.tree-panel { grid-column: 1; }.viewport-wrap { grid-column: 2; min-height: 620px; }.details-panel { grid-column: 1 / -1; border-top: 1px solid #202832; }.details { max-height: 55vh; } }
@media (max-width: 560px) { .top-actions { gap: 0; }.workspace { grid-template-columns: 1fr; grid-template-rows: auto minmax(420px, 62vh) auto; }.tree-panel { grid-column: 1; grid-row: 1; border-bottom: 1px solid #202832; }.tree { max-height: 210px; }.viewport-wrap { grid-column: 1; grid-row: 2; }.details-panel { grid-column: 1; grid-row: 3; }.footer-note { display: none; }.viewport-tools { left: 10px; right: 10px; overflow-x: auto; }.selection-tools { flex: 0 0 auto; }.viewport-tools > #fit-button { flex: 0 0 auto; } }
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}
+3
View File
@@ -0,0 +1,3 @@
import { defineConfig } from 'vite';
export default defineConfig({});