diff --git a/.claude/docs/INPUT.md b/.claude/docs/input.md similarity index 100% rename from .claude/docs/INPUT.md rename to .claude/docs/input.md diff --git a/.claude/docs/RENDERING_OPTIMIZATIONS.md b/.claude/docs/rendering-optimizations.md similarity index 99% rename from .claude/docs/RENDERING_OPTIMIZATIONS.md rename to .claude/docs/rendering-optimizations.md index f1a82156..9699814b 100644 --- a/.claude/docs/RENDERING_OPTIMIZATIONS.md +++ b/.claude/docs/rendering-optimizations.md @@ -199,5 +199,5 @@ All custom shader materials use `glslVersion: THREE.GLSL3`. The StandardMaterial ### Related Documentation - [CLAUDE.md](../../CLAUDE.md) - Main project documentation -- [INPUT.md](./INPUT.md) - Input system architecture +- [input.md](./input.md) - Input system architecture - [optimization.md](./optimization.md) - Loading pipeline performance diff --git a/.claude/docs/styling.md b/.claude/docs/styling.md new file mode 100644 index 00000000..1af34f95 --- /dev/null +++ b/.claude/docs/styling.md @@ -0,0 +1,197 @@ +# Styling Guide + +Reference for working with `style.css` in the react-viewers layer. Covers the token system, layout patterns, third-party override conventions, and known pitfalls. + +--- + +## Token System + +All design values live in `:root` at the top of `style.css`. Never use raw values in component rules. + +### Colors +```css +/* Brand */ +--c-primary, --c-primary-royal, --c-secondary-yellow + +/* Grays (darkest → lightest) */ +--c-darkest-gray, --c-darker-gray, --c-dark-gray-warm, --c-dark-gray-cool, +--c-medium-gray, --c-gray-divider, --c-light-gray, --c-lightest-gray, --c-white + +/* Accents */ +--c-lightest-blue /* selection highlight background */ +--c-list-hover /* warm yellow for hovered list rows */ +--c-overflow /* dark overlay for blocking UI */ +--c-focus-ring /* subtle blue for focus-ring-style effects */ +``` + +### Spacing / Gap +Only two gap values are used: +```css +--gap-xs: 0.25rem; /* 4px — inline/icon gaps */ +--gap-sm: 0.625rem; /* 10px — standard item gaps */ +``` +Use `gap: 0` (bare zero, no unit) for explicit zero gaps. + +### Z-index Scale +```css +--z-overlay: 10; /* loading overlay */ +--z-ui: 20; /* control bar, axes panel, side panel */ +--z-panel: 30; /* panel overlay container, side panel nav */ +--z-modal: 40; /* modal dialogs */ +--z-popup: 50; /* dropdowns, context menus */ +``` + +### Typography +```css +--font-size-xs / sm / base / lg / xl +--font-weight-medium: 500 +--font-weight-semibold: 600 +``` + +### Other +```css +--radius-sm: 4px | --radius-md: 6px | --radius-full: 9999px +--shadow-sm / md / lg +--transition-fast: 0.15s | --transition-base: 0.2s +--size-control: 32px /* standard button/row height */ +``` + +--- + +## Panel / Entry Architecture + +Three panel instances share the same base class structure (`vim-panel-*`) with instance-specific overrides: + +``` +.vim-settings-panel — main settings popup (settings + isolation entries) +.vim-bim-header — flat readonly entries (name / id / category) +.vim-bim-body — group → section → zebra entries (BIM parameters) +``` + +### Class Hierarchy +``` +.vim-panel-list flex column container + .vim-panel-group collapsible group (details/summary) + .vim-panel-section collapsible section (details/summary) + .vim-panel-entry dt/dd row +``` + +### Entry Layout +Base: `dt` = 50% fixed, `dd` = flex row, centered, `--gap-xs` gap. +Override for BIM panels: `dd` → `display: block` (single text value, no input). + +### Zebra Striping +Panel background is `--c-lightest-gray`. Rows alternate: +```css +.vim-panel-entry:nth-child(odd) { background: var(--c-white); } +.vim-panel-entry:nth-child(even) { background: transparent; } +``` +Transparent even rows show the panel background. **If the panel background is white, zebra will be invisible.** Keep `.vim-panel` background at `--c-lightest-gray`. + +BIM body uses the same pattern on `.vim-bim-body-entry`. BIM tree uses the same on `li` items. + +### Section Title Style +The shared override block (below the base classes) applies two styled variants to both `.vim-settings-panel` and `.vim-bim-body`: +- **Tab-style group title**: `inline-flex`, bordered top, rounded top corners +- **Filled section title**: solid `--c-light-gray` background, small-caps label + +--- + +## `vim-hidden` Pattern + +The `vim-hidden` class is used to toggle component visibility from JS: +```ts +el.classList.toggle('vim-hidden', !shouldShow) +``` + +**Each component that uses this pattern needs its own CSS rule.** There is no global `.vim-hidden { display: none }` — because `vim-hidden` is also used as a *state* class on BIM tree visibility icons (where it means "this element is currently hidden" — not that the icon itself should be hidden). + +```css +/* Correct pattern — component-scoped */ +.vim-axes-panel.vim-hidden { display: none; } +.vim-performance-div.vim-hidden { display: none; } +``` + +If you add a new toggleable component, add a matching scoped rule. If the toggle doesn't work, this is the first thing to check. + +--- + +## Third-Party Library Overrides (`!important`) + +Three blocks use `!important` to override library styles. All are documented with comments explaining why: + +| Selector | Library | Why | +|---|---|---| +| `.__react_component_tooltip::after/before` | react-tooltip | Removes shadow arrow cutting into tooltip text | +| `.vim-tooltip` | react-tooltip | Overrides library's own inline/default styles | +| `.vim-bim-tree .rct-tree-item-button` | react-complex-tree | Overrides library's inline `display` and justify | +| `.vim-performance` | stats-js / react-tooltip | Overrides inline positioning | + +**Rule**: Only use `!important` for third-party overrides. Always add a comment explaining which library and why. + +--- + +## Tree Visibility State Machine + +The BIM tree visibility toggle icon has two CSS classes (`vim-visible` / `vim-hidden`) and three context-dependent hover states. The CSS for this is in the "Tree View Visibility Toggles" section. Key points: + +- Default `vim-visible` → `display: none` (hidden until hover) +- Default `vim-hidden` → always visible with a "hidden" icon +- Hover color changes based on selection state: beige → light-blue → royal-blue + +SVG icons for the visibility eye are stored as CSS custom properties (the `--visible-*` and `--hidden-*` variables at the top of `:root`). They're long but correctly placed there. + +--- + +## Responsive Scaling + +The viewer uses **container queries** (not media queries) throughout. Container types are set on wrapper divs: + +```css +.vim-side-panel { container-type: size; } +.vim-modal { container-type: size; } +.vim-loading-container { container-type: size; } +``` + +The responsive block at the bottom (`@container (width > 0)`) uses `min()` with `cqmin`/`cqh` units to scale gaps and font sizes fluidly: +```css +gap: min(2cqmin, 4px); +font-size: min(5cqmin, 1rem); +``` + +--- + +## Focus / Accessibility + +Focus rings are **intentionally suppressed**. This viewer is pointer/touch-driven. Hover feedback is implemented manually on all interactive elements. Keyboard navigation is not a supported interaction mode. + +```css +/* Intentional — do not add :focus-visible styles */ +.vim-component button:focus { outline: 0; box-shadow: none; } +``` + +--- + +## Generic Number Entry — Range Display + +`GenericNumberEntry` auto-derives a range hint from `min`/`max` when `info` is not explicitly set: + +| Fields | Displayed | +|---|---| +| `min` + `max` | `[0, 1]` | +| `min` only | `≥ 0` | +| `max` only | `≤ 5` | +| explicit `info` | that string (takes priority) | + +When adding number entries, prefer `min`/`max` over a manual `info` string — it also enforces the constraint natively in the HTML input. + +--- + +## Code Style Rules + +- **Zero values**: always bare `0`, never `0px` +- **Gap values**: always `var(--gap-xs)` or `var(--gap-sm)`, never raw `4px`/`8px`/`0.25rem`/`0.5rem` +- **Colors**: always `var(--c-*)`, never raw hex or rgb +- **Font weights**: always `var(--font-weight-medium/semibold)` for 500/600 +- **Z-index**: always `var(--z-*)`, never raw numbers +- **Magic numbers**: add an inline comment explaining the value (e.g. `/* chevron size */`, `/* 33% label / 67% value split */`) diff --git a/.claude/docs/webgl-vs-ultra.md b/.claude/docs/webgl-vs-ultra.md new file mode 100644 index 00000000..4259f929 --- /dev/null +++ b/.claude/docs/webgl-vs-ultra.md @@ -0,0 +1,200 @@ +# WebGL vs Ultra: Discrepancies and Commonalities + +## Overview + +WebGL renders locally via Three.js; Ultra streams from a server via WebSocket RPC. This fundamental difference drives most divergence, but shared abstractions keep the React layer consistent where it matters. + +--- + +## What's Well Aligned + +| Aspect | Notes | +|--------|-------| +| **Selection** | Both use `ISelection` generic interface from `shared/selection.ts` | +| **Input system** | Both use `IInputHandler` from `shared/input/` | +| **Framing** | Both use `FramingApi` + adapter pattern via `state/cameraState.ts` | +| **Isolation** | Both use `IsolationApi` + `IIsolationAdapter` + `useSharedIsolation()` | +| **Section box** | Both use `SectionBoxApi` + adapter pattern via `state/sectionBoxState.ts` | +| **Event system** | Both use `ISignal`/`ISimpleEvent` from ste-signals | +| **VIM collection** | Both use `VimCollection` | +| **Load pattern** | Both wrap core load with React UI (progress, errors) | +| **Settings** | Both use `useSettings()` + `SettingsApi` | +| **Control bar** | Both use shared builder functions (`controlBarCamera`, `controlBarSectionBox`, `controlBarVisibility`) | +| **Import discipline** | React layer consistently uses barrel imports | + +--- + +## ViewerApi Shape + +| Field | WebGL | Ultra | Notes | +|-------|:-----:|:-----:|-------| +| `type` | `'webgl'` | `'ultra'` | Discriminant | +| `container` | ✓ | ✓ | | +| `core` | `Webgl.Viewer` | `Ultra.Viewer` | Different types | +| `load` | ✓ | ✓ | Different signatures | +| `open` | ✓ | ✗ | Load without geometry (WebGL only) | +| `unload` | ✓ | ✓ | | +| `framing` | ✓ | ✓ | | +| `isolation` | ✓ | ✓ | | +| `sectionBox` | ✓ | ✓ | | +| `controlBar` | ✓ | ✓ | | +| `modal` | ✓ | ✓ | | +| `contextMenu` | ✓ | ✗ | WebGL only | +| `bimInfo` | ✓ | ✗ | WebGL only | +| `isolationPanel` | ✓ | ✓ | | +| `sectionBoxPanel` | ✓ | ✓ | | +| `settings` | ✓ | ✓ | | +| `ui` | `WebglUiApi` | `UltraUiApi` | Different observable fields | +| `dispose` | ✓ | ✓ | | + +WebGL-only features (contextMenu, bimInfo, open) are intentional — Ultra renders server-side so BIM data and deferred geometry loading aren't available client-side. + +--- + +## Element3D + +| Property/Method | WebGL | Ultra | Notes | +|----------------|:-----:|:-----:|-------| +| `vim` | ✓ | ✓ | Different vim types | +| `element` | ✓ | ✓ | Element index | +| `elementId` | ✓ | ✗ | | +| `elementUniqueId` | ✓ | ✗ | | +| `instances` | ✓ | ✗ | | +| `hasGeometry` | ✓ | ✗ | | +| `hasMesh` | ✓ | ✗ | | +| `isRoom` | ✓ | ✗ | | +| `visible` | ✓ | ✓ | | +| `outline` | ✓ | ✓ | | +| `focused` | ✓ | ✗ | | +| `ghosted` | ✗ | ✓ | Ultra-specific state | +| `color` | `THREE.Color` | `THREE.Color` | Same type | +| `getBoundingBox()` | ✓ | ✓ | | +| `getCenter()` | ✓ | ✗ | | +| `getBimElement()` | ✓ | ✗ | No BIM data client-side | +| `getBimParameters()` | ✓ | ✗ | No BIM data client-side | + +Ultra elements are intentionally thin — the server owns geometry and BIM data. + +--- + +## Camera + +| Capability | WebGL | Ultra | +|-----------|:-----:|:-----:| +| `snap()` / `lerp(duration)` | ✓ | ✓ | +| `frame(target)` | ✓ | ✓ | +| `set(position, target)` | ✓ | ✓ | +| `reset()` | ✓ | ✓ | +| `move()` | ✓ | ✗ | +| `rotate()` | ✓ | ✗ | +| `zoom()` / `zoomTowards()` | ✓ | ✗ | +| `orbit()` / `orbitTowards()` | ✓ | ✗ | +| `lookAt()` / `setTarget()` | ✓ | ✗ | +| `orthographic` | ✓ | ✗ | +| `lockRotation` | ✓ | ✗ | +| `pause()` | ✗ | ✓ | +| `save()` | ✗ | ✓ | + +Ultra's camera is minimal because movement commands go through RPC. WebGL has a rich local camera with fluent API. + +--- + +## Discrepancies Worth Noting + +### 1. Selection Outline Mechanism + +**WebGL:** Direct `outline` boolean property on Element3D. +```typescript +object.outline = state +``` + +**Ultra:** Bit-flag `VisibilityState` enum manipulated by selection adapter. +```typescript +object.state = state + ? (HIGHLIGHTED | currentVisibility) + : (currentVisibility & ~HIGHLIGHTED) +``` + +Different models for the same user-facing concept. + +### 2. Isolation Adapter State Management + +**WebGL:** Plain closure variables. +```typescript +let ghost = false +let transparency: 'all' | 'opaque' | 'transparent' = 'all' +``` + +**Ultra:** `createState()` for ghost flag. +```typescript +const ghost = createState(false) +``` + +Mixed patterns for equivalent adapters. + +### 3. Ultra Stub Implementations + +The Ultra isolation adapter has placeholder methods that silently do nothing: +```typescript +setTransparency: (enabled) => { console.log("not implemented") } +getShowRooms: () => true +setShowRooms: (show) => { console.log("not implemented") } +``` + +These make the `IIsolationAdapter` contract misleading — settings are accepted but ignored. + +### 4. Camera frame() Return Types + +- **WebGL:** `frame()` returns `Promise` +- **Ultra:** `frame()` returns `Promise` + +Different return types for the same framing concept. + +### 5. UI State Hooks + +- `useWebglUiState()` returns `{ ui, refs, uiValues }` +- `useUltraUiState()` returns `{ ui, uiValues }` (no `refs` — no localStorage persistence) + +### 6. Control Bar + +WebGL has extra sections: measure tool, pointer mode, axes controls. +Ultra omits these (server-side rendering doesn't support client-side measurement). + +Both share the builder functions for camera, section box, and visibility — good reuse. + +### 7. Core Viewer Interface + +| WebGL has | Ultra has instead | +|-----------|-------------------| +| `materials` | (server-side) | +| `gizmos` | (not applicable) | +| | `decoder` (RPC decoding) | +| | `serverUrl`, `connectionState` | +| | `connect()`, `disconnect()` | +| | `sectionBox` (direct property, not gizmo) | + +--- + +## Settings + +**WebGL-only settings:** +- `panelBimTree`, `panelBimInfo`, `panelPerformance`, `panelAxes` +- `axesOrthographic`, `axesHome` +- `miscProjectInspector`, `miscMaximise` +- Measure tool section + +**Shared settings:** +- `panelLogo`, `panelControlBar` +- `miscSettings`, `miscHelp` +- Camera, cursor, section, visibility control bar sections + +--- + +## Summary + +Most discrepancies are **intentional** — Ultra is a thin client that delegates to a server. The shared abstractions (selection, isolation, framing, section box) are well-designed with adapter patterns that absorb the differences. + +The **actionable** inconsistencies are: +1. **Stub methods** in Ultra isolation — either implement or narrow the adapter interface +2. **Mixed state patterns** in adapters (closures vs createState) +3. **Camera return types** — `frame()` should return consistent types through the framing API diff --git a/.claude/skills/auto-refactor/skill.md b/.claude/skills/auto-refactor/SKILL.md similarity index 100% rename from .claude/skills/auto-refactor/skill.md rename to .claude/skills/auto-refactor/SKILL.md diff --git a/.claude/skills/css/skill.md b/.claude/skills/css/SKILL.md similarity index 100% rename from .claude/skills/css/skill.md rename to .claude/skills/css/SKILL.md diff --git a/.gitignore b/.gitignore index 4699958f..00b63a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ node_modules dist -docs -docs2 +/docs +/docs2 src/pages/*.vim .claude/settings.json .claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md index a9cf758d..cdafb8f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -274,7 +274,7 @@ state.useMemo((v) => compute(v)) ## Input System -> **📖 Full Documentation**: See [INPUT.md](./.claude/docs/INPUT.md) for architecture, patterns, and advanced customization +> **📖 Full Documentation**: See [.claude/docs/input.md](./.claude/docs/input.md) for architecture, patterns, and advanced customization ### Default Bindings @@ -335,7 +335,7 @@ viewer.core.inputs.mouse.onClick = (pos) => { /* custom logic */ } // Restore: viewer.core.inputs.pointerMode = originalMode ``` -See [INPUT.md](./.claude/docs/INPUT.md) for more patterns, coordinate systems, performance optimization, and debugging techniques +See [.claude/docs/input.md](./.claude/docs/input.md) for more patterns, coordinate systems, performance optimization, and debugging techniques --- @@ -566,7 +566,7 @@ npm run documentation # TypeDoc ### Loading Pipeline (WebGL) -> **📖 Loading Optimization**: See [.claude/optimization.md](./.claude/docs/optimization.md) for geometry building performance, lazy Element3D creation, and profiling techniques +> **📖 Loading Optimization**: See [.claude/docs/optimization.md](./.claude/docs/optimization.md) for geometry building performance, lazy Element3D creation, and profiling techniques Full call chain from `viewer.load()` to rendered scene: @@ -607,7 +607,7 @@ await vim.load(sub) ### Rendering Pipeline (WebGL) -> **📖 Optimization Guide**: See [.claude/RENDERING_OPTIMIZATIONS.md](./.claude/docs/RENDERING_OPTIMIZATIONS.md) for shader optimizations, GLSL3 migration, and performance improvements +> **📖 Optimization Guide**: See [.claude/docs/rendering-optimizations.md](./.claude/docs/rendering-optimizations.md) for shader optimizations, GLSL3 migration, and performance improvements Multi-pass compositor: ``` diff --git a/MIGRATION.md b/MIGRATION.md index c046998f..f02a3645 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,4 +1,4 @@ -# Migration Guide: vim-web 0.5 → 1.0.0-beta.1 +# Migration Guide: vim-web 0.5 → 1.0.0-beta.4 ## Install @@ -185,9 +185,26 @@ All Tailwind utility classes (`vc-flex`, `vc-text-sm`, etc.) have been replaced ## Peer Dependencies -| | 0.5 | 1.0-beta.1 | -|---|---|---| -| react | ^18.3.1 | ^18.3.1 \|\| ^19.0.0 | -| react-dom | ^18.3.1 | ^18.3.1 \|\| ^19.0.0 | +| | 0.5 | 1.0-beta.1 | 1.0-beta.4 | +|---|---|---|---| +| react | ^18.3.1 | ^18.3.1 \|\| ^19.0.0 | ^18.3.1 \|\| ^19.0.0 | +| react-dom | ^18.3.1 | ^18.3.1 \|\| ^19.0.0 | ^18.3.1 \|\| ^19.0.0 | +| three | (bundled) | (bundled) | ^0.183 | React 18.3+ continues to work. React 19 is now also supported. + +### three is now a peer dependency (1.0.0-beta.4) + +Through beta.3, `three` was bundled inside vim-web. As of beta.4 it is a peer dependency the host app must install: + +```bash +npm install three @types/three +``` + +This keeps a single instance of three in your app. Previously an app that already used three ended up with two copies — breaking `instanceof` checks across the boundary (including against `VIM.THREE`), duplicating three's module-level state, and shipping ~1.8 MB twice. + +Only the pinned version (`^0.183`) is tested; other three.js versions may work. If your app pins a different three, override the peer range at your own risk — a single shared copy is still preferable to a bundled duplicate. + +### IIFE build removed (1.0.0-beta.4) + +The `