3d2d4d6321
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
582 lines
18 KiB
Markdown
582 lines
18 KiB
Markdown
# WebGL2 GPU-Accelerated 2D Plan Renderer — Architecture
|
||
|
||
## Executive Summary
|
||
|
||
A WebGL2 canvas renderer for PlanView's heavy geometry (polygons, lines, hatches) with CPU fallback. Geometry tessellates once per plan and caches; pan/zoom only updates a transform-matrix uniform. Screen-space stroke width via vertex shader normal expansion. Thin SVG overlay handles text, grips, snap markers, tool preview.
|
||
|
||
**No npm dependencies** — raw WebGL2 + TypeScript.
|
||
|
||
---
|
||
|
||
## Current State (SVG Bottleneck)
|
||
|
||
**PlanView.tsx** renders `Primitive[]` (polygon/line/arc/text) → SVG DOM:
|
||
- ~2500 LOC: pan/zoom via viewBox, toScreen() scaling (1 meter = 90 viewBox units)
|
||
- `Primitive` types (generatePlan.ts:133):
|
||
- **polygon**: `pts: Vec2[]`, fill/stroke/strokeWidthMm, hatch (solid/insulation/diagonal/crosshatch)
|
||
- **line**: a/b endpoints, className, weightMm, dash[], optional color
|
||
- **arc**: center, from/to points, r, className, weightMm, dash[]
|
||
- **text**: anchor, RichTextDoc, roomStamp metadata
|
||
- Bottleneck: **pan/zoom re-renders entire SVG DOM** → Cairo rasterizes geometry at 144 Hz
|
||
|
||
**Key constants:**
|
||
- `PX_PER_M = 90` (viewBox units per meter; Modell-Y up → SVG-Y down via negation)
|
||
- `PAD = 60` (margin in viewBox units)
|
||
- `mmToPx(mm) = (mm / 25.4) * dpi()` (stroke width: constant screen-px via non-scaling-stroke)
|
||
- `ZOOM_MAX/MIN = 50/0.2` (pan/zoom bounds)
|
||
|
||
---
|
||
|
||
## Architecture: PlanRenderer (WebGL2 + Fallback SVG)
|
||
|
||
### Module Structure
|
||
|
||
```
|
||
src/plan/
|
||
├── PlanRenderer.ts (Main GPU/CPU dispatcher)
|
||
├── glPlan/
|
||
│ ├── glPlanCompile.ts (Tessellation & buffer upload)
|
||
│ ├── glPlanShaders.ts (Vertex/fragment sources + compilation)
|
||
│ ├── glPlanRender.ts (Draw loop: matrix uniform, state mgmt)
|
||
│ └── glPlanTypes.ts (TypeScript interfaces for GPU data)
|
||
└── PlanView.tsx (React wrapper, unchanged API)
|
||
```
|
||
|
||
### High-Level Flow
|
||
|
||
```
|
||
PlanView.tsx
|
||
↓ [receives plan: Plan]
|
||
↓
|
||
PlanRenderer (new abstraction)
|
||
↓
|
||
├─→ GPU path [if WebGL2 available && flag=true]
|
||
│ ├─ glPlanCompile() → upload tessellated geometry to VRAM
|
||
│ ├─ glPlanRender() → draw with pan/zoom matrix uniform
|
||
│ └─ [fast pan/zoom via matrix only]
|
||
│
|
||
└─→ Fallback: SVG [if WebGL fails || flag=false]
|
||
└─ existing PlanView render path (toScreen + DOM)
|
||
```
|
||
|
||
---
|
||
|
||
## GPU Path: Tessellation & Shaders
|
||
|
||
### 1. Tessellation Strategy
|
||
|
||
#### **Polygon** → Fans + Ear Clipping
|
||
- **Input**: Primitive.polygon = { pts: Vec2[], fill, stroke, strokeWidthMm, hatch }
|
||
- **Output**: Indexed triangle mesh
|
||
- **Algorithm**: Earcut2D (existing JS library logic, inlined to avoid npm)
|
||
- Convert polygon pts to 2D float32 array in **world space** (meters)
|
||
- Earcut → triangle indices
|
||
- Store: `{ vertices: Float32Array, indices: Uint32Array, color: vec4, hasHatch: bool }`
|
||
- **Hatch rendering**: Bake hatch as texture or re-implement in fragment shader (MVP: solid fill only; hatch deferred)
|
||
|
||
#### **Line** → Quad Expansion (Screen-Space Width)
|
||
- **Input**: Primitive.line = { a, b, weightMm, cls, dash?, color }
|
||
- **Output**: Degenerate quad (2 triangles) with screen-space normal offset
|
||
- **Strategy**:
|
||
1. Vertex shader receives `{ pos: vec2, side: float }` (side = ±1 for left/right edge)
|
||
2. Transform pos to clip space via matrix uniform
|
||
3. Compute screen-space perpendicular via `dFdx/dFdy` or pre-compute normal in CPU
|
||
4. Expand by `(weightMm / 25.4) * dpi * (screenPixelsPerClipUnit)` in clip space
|
||
5. Fragment shader: solid color (no dash MVP; dashing deferred or CPU pre-tessellation)
|
||
|
||
#### **Arc** → Line Segments (Polyline → Quads)
|
||
- **Input**: Primitive.arc = { center, from, to, r, weightMm, cls, dash }
|
||
- **Output**: Tessellate arc to ~30 line segments (adaptive based on radius/zoom), expand each as quad
|
||
- Fallback: SVG arc for MVP
|
||
|
||
#### **Text, Grips, Snap-Markers, Tool-Preview**
|
||
- **Stays in SVG overlay** (thin, non-bottleneck)
|
||
- Render above WebGL canvas at z-order 1
|
||
|
||
---
|
||
|
||
### 2. Shader Sources (GLSL 3.00 ES)
|
||
|
||
#### **Vertex Shader: Solid Fill (polygon)**
|
||
```glsl
|
||
#version 300 es
|
||
precision highp float;
|
||
|
||
uniform mat4 viewProjection; // pan/zoom as 2×3 affine (expand to mat4)
|
||
|
||
layout(location=0) in vec2 position; // world-space (meters)
|
||
layout(location=1) in vec4 color; // fill color
|
||
|
||
out VS_OUT {
|
||
flat vec4 vertexColor;
|
||
} vs_out;
|
||
|
||
void main() {
|
||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||
gl_Position = clipPos;
|
||
vs_out.vertexColor = color;
|
||
}
|
||
```
|
||
|
||
#### **Vertex Shader: Screen-Space Stroked Line**
|
||
```glsl
|
||
#version 300 es
|
||
precision highp float;
|
||
|
||
uniform mat4 viewProjection; // world → clip space
|
||
uniform vec2 screenSize; // canvas (width, height) in pixels
|
||
uniform float strokeWidthMm; // millimeters
|
||
uniform float dpi; // 96 * devicePixelRatio
|
||
|
||
layout(location=0) in vec2 position; // world-space endpoint
|
||
layout(location=1) in float sideFlag; // ±1.0 (left/right edge)
|
||
layout(location=2) in vec4 lineColor; // stroke color
|
||
|
||
out VS_OUT {
|
||
flat vec4 vertexColor;
|
||
} vs_out;
|
||
|
||
void main() {
|
||
vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0);
|
||
|
||
// Convert stroke width (mm) → screen pixels
|
||
float strokePx = (strokeWidthMm / 25.4) * dpi;
|
||
// Convert screen pixels → normalized device coords (NDC)
|
||
// NDC ∈ [-1,1]²; screen (0,screenSize) → NDC [-1,1]
|
||
float strokeNdc = (strokePx / screenSize.x) * 2.0;
|
||
|
||
// Expand in clip space (simple; assumes aspect ≈ 1)
|
||
vec4 expanded = clipPos + vec4(sideFlag * strokeNdc, 0.0, 0.0, 0.0);
|
||
|
||
gl_Position = expanded;
|
||
vs_out.vertexColor = lineColor;
|
||
}
|
||
```
|
||
|
||
#### **Fragment Shader (both)**
|
||
```glsl
|
||
#version 300 es
|
||
precision highp float;
|
||
|
||
in VS_OUT {
|
||
flat vec4 vertexColor;
|
||
} fs_in;
|
||
|
||
out vec4 fragColor;
|
||
|
||
void main() {
|
||
fragColor = fs_in.vertexColor;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. GPU Data Structures (TypeScript)
|
||
|
||
**glPlanTypes.ts:**
|
||
```typescript
|
||
export interface GLGeometryBatch {
|
||
/** Vertex buffer: interleaved (x, y, [z if 3D], ...) in world space. */
|
||
vertexBuffer: WebGLBuffer;
|
||
vertexCount: number;
|
||
|
||
/** Index buffer (triangles for fill, degenerate quads for strokes). */
|
||
indexBuffer: WebGLBuffer;
|
||
indexCount: number;
|
||
|
||
/** Vertex Array Object (VAO) binds VBO + IBO. */
|
||
vao: WebGLVertexArrayObject;
|
||
|
||
/** Per-batch metadata. */
|
||
batches: Array<{
|
||
kind: "polygon" | "line" | "arc";
|
||
indexStart: number;
|
||
indexCount: number;
|
||
color: [r: number, g: number, b: number, a: number]; // RGBA [0,1]
|
||
hasHatch: boolean;
|
||
hatchPattern?: "solid" | "insulation" | "diagonal" | "crosshatch";
|
||
strokeWidthMm?: number;
|
||
}>;
|
||
}
|
||
|
||
export interface GLPlanRenderState {
|
||
// Pan/zoom transform: world (meters) → clip space
|
||
viewMatrix: Matrix3 | Matrix4; // 2×3 affine
|
||
projMatrix: Matrix4; // orthographic
|
||
|
||
// Viewport size & DPI for screen-space stroke width
|
||
screenWidth: number;
|
||
screenHeight: number;
|
||
dpi: number;
|
||
|
||
// Compiled shaders
|
||
solidFillProgram: WebGLProgram;
|
||
strokeProgram: WebGLProgram;
|
||
|
||
// Geometry cache (tessellated once per plan)
|
||
geometryBatch: GLGeometryBatch | null;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## MVP API: PlanRenderer Class
|
||
|
||
### Interface
|
||
|
||
```typescript
|
||
export class PlanRenderer {
|
||
/**
|
||
* Create renderer with WebGL2 context + fallback config.
|
||
*/
|
||
constructor(
|
||
canvas: HTMLCanvasElement,
|
||
options?: {
|
||
enableGpu?: boolean; // default: true
|
||
enableGpuFallback?: boolean; // SVG fallback if GL fails
|
||
}
|
||
);
|
||
|
||
/**
|
||
* Compile and cache geometry from primitives.
|
||
* Call once per plan change.
|
||
*/
|
||
compilePlan(plan: Plan): Promise<void>;
|
||
|
||
/**
|
||
* Set pan/zoom transform matrix.
|
||
* Call on every view change (pan, zoom, fit).
|
||
*/
|
||
setViewMatrix(viewBox: { x, y, w, h }, canvasSize: { w, h }): void;
|
||
|
||
/**
|
||
* Render one frame: clear, draw batches, composite.
|
||
* Called from requestAnimationFrame loop.
|
||
*/
|
||
render(): void;
|
||
|
||
/**
|
||
* Release WebGL resources.
|
||
*/
|
||
dispose(): void;
|
||
|
||
/**
|
||
* Query GPU availability / fallback state.
|
||
*/
|
||
isGpuReady(): boolean;
|
||
isFallbackActive(): boolean;
|
||
}
|
||
```
|
||
|
||
### Usage in PlanView
|
||
|
||
**Before** (SVG only):
|
||
```tsx
|
||
function PlanView({ plan, ... }) {
|
||
return (
|
||
<svg ref={svgRef}>
|
||
<defs>{hatches}</defs>
|
||
{plan.primitives.map((p, i) => <PrimitiveShape ... />)}
|
||
</svg>
|
||
);
|
||
}
|
||
```
|
||
|
||
**After** (GPU + SVG fallback):
|
||
```tsx
|
||
function PlanView({ plan, ... }) {
|
||
const rendererRef = useRef<PlanRenderer | null>(null);
|
||
|
||
useEffect(() => {
|
||
const canvas = canvasRef.current;
|
||
if (!canvas) return;
|
||
rendererRef.current = new PlanRenderer(canvas, { enableGpu: true });
|
||
rendererRef.current.compilePlan(plan);
|
||
}, [plan]);
|
||
|
||
useEffect(() => {
|
||
rendererRef.current?.setViewMatrix(view, { w: canvasWidth, h: canvasHeight });
|
||
}, [view, canvasWidth, canvasHeight]);
|
||
|
||
useEffect(() => {
|
||
const frame = () => {
|
||
rendererRef.current?.render();
|
||
rafId = requestAnimationFrame(frame);
|
||
};
|
||
rafId = requestAnimationFrame(frame);
|
||
return () => cancelAnimationFrame(rafId);
|
||
}, []);
|
||
|
||
return (
|
||
<div style={{ position: "relative" }}>
|
||
{/* GPU canvas (or SVG fallback if GL unavailable) */}
|
||
<canvas ref={canvasRef} style={{ position: "absolute" }} />
|
||
{/* Thin SVG overlay: text, grips, snap-markers, tool preview */}
|
||
<svg ref={svgRef} style={{ position: "absolute", zIndex: 1 }}>
|
||
{/* text, grips, snaps only; geometry stays in WebGL */}
|
||
</svg>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Data Flow: From Primitives → GPU
|
||
|
||
### 1. **Compile Phase** (glPlanCompile.ts)
|
||
|
||
```typescript
|
||
export function compilePlan(gl: WebGL2RenderingContext, plan: Plan): GLGeometryBatch {
|
||
const batches: BatchInfo[] = [];
|
||
const vertices: number[] = [];
|
||
const indices: number[] = [];
|
||
let indexOffset = 0;
|
||
|
||
for (const prim of plan.primitives) {
|
||
if (prim.kind === "polygon") {
|
||
const { verts, inds } = tessellatePolygon(prim.pts);
|
||
const color = parseColor(prim.fill);
|
||
batches.push({
|
||
kind: "polygon",
|
||
indexStart: indexOffset,
|
||
indexCount: inds.length,
|
||
color,
|
||
hasHatch: prim.hatch.pattern !== "none",
|
||
hatchPattern: prim.hatch.pattern,
|
||
});
|
||
vertices.push(...verts);
|
||
indices.push(...inds.map((i) => i + indexOffset));
|
||
indexOffset += verts.length / 2;
|
||
} else if (prim.kind === "line") {
|
||
const { verts, inds } = tessellateLineQuad(prim.a, prim.b);
|
||
const color = parseColor(prim.color || "black");
|
||
batches.push({
|
||
kind: "line",
|
||
indexStart: indexOffset,
|
||
indexCount: inds.length,
|
||
color,
|
||
strokeWidthMm: prim.weightMm,
|
||
});
|
||
vertices.push(...verts);
|
||
indices.push(...inds.map((i) => i + indexOffset));
|
||
indexOffset += verts.length / 2;
|
||
}
|
||
// arc → polyline → quads (deferred for MVP)
|
||
}
|
||
|
||
const vbo = gl.createBuffer()!;
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
||
|
||
const ibo = gl.createBuffer()!;
|
||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
|
||
|
||
const vao = gl.createVertexArray()!;
|
||
gl.bindVertexArray(vao);
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0); // position
|
||
gl.enableVertexAttribArray(0);
|
||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
||
|
||
return { vertexBuffer: vbo, indexBuffer: ibo, vao, batches, vertexCount: vertices.length, indexCount: indices.length };
|
||
}
|
||
```
|
||
|
||
### 2. **Render Phase** (glPlanRender.ts)
|
||
|
||
```typescript
|
||
export function renderPlan(
|
||
gl: WebGL2RenderingContext,
|
||
state: GLPlanRenderState,
|
||
batch: GLGeometryBatch
|
||
): void {
|
||
gl.clearColor(1, 1, 1, 1); // white background
|
||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||
|
||
gl.useProgram(state.solidFillProgram);
|
||
const mvpLoc = gl.getUniformLocation(state.solidFillProgram, "viewProjection");
|
||
const mvp = mat4.multiply(state.projMatrix, state.viewMatrix);
|
||
gl.uniformMatrix4fv(mvpLoc, false, mvp);
|
||
|
||
gl.bindVertexArray(batch.vao);
|
||
|
||
for (const b of batch.batches) {
|
||
const colorLoc = gl.getUniformLocation(state.solidFillProgram, "vertexColor");
|
||
gl.uniform4f(colorLoc, b.color[0], b.color[1], b.color[2], b.color[3]);
|
||
|
||
gl.drawElements(gl.TRIANGLES, b.indexCount, gl.UNSIGNED_INT, b.indexStart * 4);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Tessellation Details
|
||
|
||
### Earcut (Polygon Triangulation)
|
||
|
||
**Inlined earcut logic (no npm):**
|
||
```typescript
|
||
function tessellatePolygon(pts: Vec2[]): { verts: number[]; inds: number[] } {
|
||
// Convert Vec2[] → flat float array
|
||
const coords = pts.flatMap((p) => [p.x, p.y]);
|
||
|
||
// Earcut2D: robust polygon triangulation
|
||
// → Returns index array (triplets = triangles)
|
||
const triangles = earcut(coords);
|
||
|
||
// Vertex buffer: just positions (x, y) in world space (meters)
|
||
const verts = coords;
|
||
|
||
return { verts, inds: triangles };
|
||
}
|
||
|
||
// Simplified earcut (full version ~200 LOC; reference libtess2 or earcut.js)
|
||
function earcut(data: number[], hole?: number[], dim?: number): number[] {
|
||
// ... iterative ear clipping, complexity O(n²) worst-case
|
||
// Returns Uint32Array of triangle indices
|
||
}
|
||
```
|
||
|
||
### Line Quad Expansion
|
||
|
||
```typescript
|
||
function tessellateLineQuad(
|
||
a: Vec2, b: Vec2,
|
||
widthMm: number = 0.5
|
||
): { verts: number[]; inds: number[] } {
|
||
// World-space endpoints; width (mm) will be expanded in vertex shader
|
||
|
||
// Create a degenerate quad: 2 triangles
|
||
// Vertices: [a_left, a_right, b_left, b_right]
|
||
// (normal expansion happens in VS)
|
||
|
||
const verts = [
|
||
a.x, a.y, 0.0, // vertex 0: a, left flag
|
||
a.x, a.y, 1.0, // vertex 1: a, right flag
|
||
b.x, b.y, 0.0, // vertex 2: b, left flag
|
||
b.x, b.y, 1.0, // vertex 3: b, right flag
|
||
];
|
||
|
||
// Two triangles: (0, 1, 2) and (1, 3, 2)
|
||
const inds = [0, 1, 2, 1, 3, 2];
|
||
|
||
return { verts, inds };
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Pan/Zoom Matrix Transform
|
||
|
||
### View Box → Clip Space
|
||
|
||
```typescript
|
||
function buildViewMatrix(
|
||
viewBox: { x, y, w, h },
|
||
canvasSize: { w, h }
|
||
): Matrix4 {
|
||
// 1. World space (meters, origin at model 0,0) → viewBox units (PX_PER_M=90)
|
||
const scale = PX_PER_M; // 1 meter → 90 viewBox units
|
||
|
||
// 2. ViewBox viewport: x,y,w,h in viewBox units → NDC [-1,+1]²
|
||
// Orthographic projection (no perspective).
|
||
const ortho = mat4.ortho(
|
||
viewBox.x,
|
||
viewBox.x + viewBox.w,
|
||
viewBox.y,
|
||
viewBox.y + viewBox.h,
|
||
-1, 1
|
||
);
|
||
|
||
// 3. Scale from viewBox units → world (invert PX_PER_M)
|
||
const scaleMatrix = mat4.scale(mat4.identity(), [1/scale, 1/scale, 1]);
|
||
|
||
return mat4.multiply(ortho, scaleMatrix);
|
||
}
|
||
```
|
||
|
||
Whenever PlanView calls `setView(viewBox)` or `onWheel()` → call `setViewMatrix()` → GPU re-renders with new matrix uniform (no tessellation).
|
||
|
||
---
|
||
|
||
## Fallback Strategy: SVG Renderer Flag
|
||
|
||
**Global flag** in PlanView or app state:
|
||
```typescript
|
||
const [useGpuRenderer, setUseGpuRenderer] = useState(true);
|
||
```
|
||
|
||
**Render path branching:**
|
||
```typescript
|
||
return useGpuRenderer && rendererRef.current?.isGpuReady()
|
||
? <canvas ref={canvasRef} />
|
||
: <svg ref={svgRef}>{/* existing SVG rendering */}</svg>;
|
||
```
|
||
|
||
**When GL fails** (e.g., no WebGL2 support, Out-Of-Memory):
|
||
1. Renderer catches error in `compilePlan()`
|
||
2. Sets internal `fallbackActive = true`
|
||
3. Returns gracefully (app renders SVG path instead)
|
||
4. User sees same plan, slower but functional
|
||
|
||
---
|
||
|
||
## Implementation Order (MVP → Iteration)
|
||
|
||
### Phase 1: Core (Week 1)
|
||
1. **glPlanTypes.ts** — TypeScript interfaces for GPU state
|
||
2. **glPlanShaders.ts** — Compile vertex/fragment shaders, handle GL errors
|
||
3. **glPlanCompile.ts** — Tessellation (earcut inlined), buffer upload
|
||
4. **glPlanRender.ts** — Draw loop, matrix uniform, clear/present
|
||
5. **PlanRenderer.ts** — Main class, dispatcher (GPU vs SVG fallback)
|
||
6. **PlanView.tsx** — Wire renderer, canvas overlay, canvas lifecycle
|
||
|
||
### Phase 2: Hatches & Lines (Week 2)
|
||
- Improve line tessellation: proper screen-space width (dFdx/dFdy or pre-computed normals)
|
||
- Hatch patterns: texture-based or procedural fragment shader (diagonal/insulation)
|
||
- Arc tessellation: polyline → quads
|
||
|
||
### Phase 3: Polish (Week 3)
|
||
- Stroke dashing via geometry or fragment shader
|
||
- Greyed opacity blending
|
||
- Hit testing integration (point-in-triangle for GPU)
|
||
- Performance profiling, batch merging
|
||
|
||
---
|
||
|
||
## Performance Targets
|
||
|
||
| Operation | SVG (Current) | GPU (Target) | Notes |
|
||
|-----------|---------------|--------------|-------|
|
||
| **Tessellation** | — | 10–50 ms | Once per plan |
|
||
| **Pan/Zoom 60 Hz** | 16 ms (re-render SVG) | <1 ms (matrix uniform) | Matrix upload negligible |
|
||
| **Pan/Zoom 144 Hz** | 7 ms (bottleneck) | <0.5 ms | 28× speedup expected |
|
||
| **Geometry: 1000 polygons** | 50–100 ms SVG render | 1–5 ms GPU draw | CPU tessellation pipelined |
|
||
|
||
User: AMD RX 7800 XT → easily capable of 4K+ geometry at 144 Hz.
|
||
|
||
---
|
||
|
||
## Known Deferred Items (Post-MVP)
|
||
|
||
- **Hatches**: Solid fill only MVP; insulation/diagonal/crosshatch in Phase 2 via texture or procedural shader
|
||
- **Dashing**: Not in MVP (complex with screen-space strokes); either CPU pre-tessellation or fragment shader alpha-discard
|
||
- **Arcs**: Fallback to SVG for MVP; GPU polyline expansion in Phase 2
|
||
- **Text, Grips, Snaps**: Stay in SVG overlay indefinitely (no GPU benefit; text rendering nontrivial)
|
||
- **Hit Testing**: Keep in CPU/SVG for MVP; GPU pick-buffer deferred
|
||
- **Color/Opacity Blending**: Basic for MVP; advanced (multiply, screen, dodge) deferred
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- **Earcut.js**: https://github.com/mapbox/earcut — polygon triangulation (logic to inline)
|
||
- **three.js line expansion**: https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLGeometries.js
|
||
- **OpenGL Perspective Division**: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Polygon_offset
|
||
- **Screen-Space Stroke Width**: https://forum.libcinder.org/topic/smooth-line-rendering-using-geometry-shaders
|
||
- PlanView source: `/home/karim/cad/src/plan/PlanView.tsx` (2500 LOC)
|
||
- Primitive types: `/home/karim/cad/src/plan/generatePlan.ts:133`
|