K3 ENGINE v0.3.0

Version: 0.3.0
Status: Development Blueprint
Last Updated: April 2026
Author: Santiago Bustelo

K3 Engine is licensed under the Apache License, Version 2.0. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0


Overview

K3 Engine is an experimental, CSS-only 3D rendering engine for the browser. It explores the boundaries of what can be achieved using pure HTML and CSS (no WebGL, no canvas) by leveraging CSS 3D transforms to construct interactive three-dimensional objects and scenes.

The project was born from a simple question: given the astonishing CSS demos produced by the creative coding community, could an entire 3D engine be built with CSS alone?

Inspirations & Acknowledgements

K3 stands on the shoulders of the CSS art and demo community. The following authors and their published work were direct inspirations for this project, and their techniques informed many of the architectural decisions made throughout the engine.

Their collective work (spanning perspective tricks, transform-origin wizardry, mathematical CSS, and pure-CSS animation) demonstrated that the browser's styling layer could be a far more expressive rendering surface than it is typically credited for.


TABLE OF CONTENTS

  1. Vision & Philosophy
  2. Current State Analysis
  3. Architecture Overview
  4. Component Specifications
  5. API Contract v1.0
  6. Test Suite & Acceptance Criteria
  7. Known Limitations

1. VISION & PHILOSOPHY

1.1 What K3 Is

K3 is a declarative 3D system for objects that live INSIDE the document, not separate from it.

Core Principles:

  1. DOM-first: 3D objects are HTML elements, not canvas textures
  2. Declarative: State is described, not imperatively constructed
  3. Honest: Limitations are documented, not hidden
  4. Educational: The code teaches, not just works

1.2 What K3 Is NOT

1.3 Target Audience

Primary:

Secondary:

1.4 Value Proposition

Three.js: Canvas-based, photorealistic, disconnected from DOM
K3: DOM-based, stylized, integrated with HTML

When to use K3:
- Technical documentation with 3D diagrams
- Scientific education (molecules, physics, anatomy)
- Architecture diagrams (system design)
- Product customization (simple, stylized)
- Indie games (flat shading aesthetic)

When to use Three.js:
- Photorealistic rendering
- Large scenes (1000+ objects)
- Advanced lighting/shadows
- Performance-critical applications

2. CURRENT STATE ANALYSIS

2.1 What EXISTS Today (from dump.txt)

✅ Core Classes:

✅ Primitives:

✅ Systems:

✅ Editor (HARDCODED):

2.2 What is BROKEN/MISSING

❌ No Public API:

// These don't exist:
K3.create()
K3.update()
K3.getState()
K3.serialize()

❌ No MutationObserver:

❌ No Error Handling:

❌ No Generative Properties:

<!-- This doesn't work: -->
<k3-use type="gear" teeth="20"></k3-use>
<!-- Changing teeth="30" doesn't rebuild geometry -->

❌ No State Snapshots:

❌ Editor is Anti-Pattern:


3. ARCHITECTURE OVERVIEW

3.1 System Layers

┌─────────────────────────────────────────────┐
│  USER CODE (HTML/JS)                         │
│  <k3-scene>, K3.create(), K3.update()        │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  PUBLIC API LAYER (v1.0 NEW)                 │
│  - Creation (create, instantiate, clone)    │
│  - State Management (get/set/update)         │
│  - Queries (pick, bounds, hierarchy)         │
│  - Serialization (export/import)             │
│  - Validation (constraints)                  │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  CORE ENGINE (exists, needs refactor)        │
│  - K3Element (TRS system)                    │
│  - K3Definition (template registry)          │
│  - K3Use (Shadow DOM instantiation)          │
│  - Signal Bus (rigging)                      │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  PRIMITIVES (exists, needs generative)       │
│  - K3Plane, K3Box, K3Sphere                  │
│  - K3Stack, K3Extrude                        │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  BROWSER (CSS 3D Transforms)                 │
│  - preserve-3d                               │
│  - matrix3d()                                │
│  - backface-visibility                       │
└─────────────────────────────────────────────┘

3.2 Data Flow

USER ACTION → PUBLIC API → VALIDATION → UPDATE PIPELINE → DOM → CSS → RENDER

Example: K3.update('box', { x: 100 })
    ↓
1. Validate: Is x a number? Is it within constraints?
2. Apply: element.setAttribute('x', 100)
3. Transform: element.updateTransform()
4. Event: K3.emit('k3:change', { id, changes })
5. Render: Browser applies matrix3d()

3.3 State Management

// STATE LIVES IN 3 PLACES:

// 1. DOM (source of truth)
<k3-box x="100" y="50" z="0"></k3-box>

// 2. Internal cache (performance)
K3._stateManager._cache = {
    'box-1': {
        position: {x: 100, y: 50, z: 0},
        rotation: {rx: 0, ry: 0, rz: 0},
        bounds: {...},
        _dirty: false
    }
}

// 3. Computed (on demand)
const state = K3.getState('box-1');
// Returns frozen snapshot

CRITICAL: DOM is source of truth, cache is optimization.

Direct DOM mutations are considered out of contract.
See: Addendum 01 — DOM Mutation Policy


4. COMPONENT SPECIFICATIONS

4.1 K3.API (NEW - v1.0)

Location: js/engine/k3-api.js

/**
 * K3 Public API
 * The ONLY way users should interact with K3.
 */

window.K3 = {
    // === CREATION ===
    
    /**
     * Create a primitive element
     * @param {string} type - 'k3-plane'|'k3-box'|'k3-sphere'|'k3-stack'|'k3-extrude'
     * @param {object} props - Initial properties
     * @param {Element} parent - Optional parent to append to
     * @returns {Element} Created element with auto-generated ID
     */
    create(type, props = {}, parent = null) {
        const element = document.createElement(type);
        element.id = props.id || `k3-${type}-${Date.now()}`;
        
        // Apply initial props
        for (const [key, value] of Object.entries(props)) {
            if (key !== 'id') {
                element.setAttribute(key, value);
            }
        }
        
        // Initialize
        if (element.render) {
            element.render();
        }
        
        if (parent) {
            parent.appendChild(element);
        }
        
        K3.emit('k3:create', { id: element.id, type });
        return element;
    },
    
    /**
     * Instantiate a registered template
     * @param {string} templateName - Name from <k3-object name="...">
     * @param {object} props - Property overrides
     * @param {Element} parent - Optional parent
     * @returns {Promise<Element>} Resolves when template is loaded
     */
    async instantiate(templateName, props = {}, parent = null) {
        // Wait for template if not loaded
        if (!K3.registry.has(templateName)) {
            await new Promise(resolve => {
                const handler = (e) => {
                    if (e.detail.name === templateName) {
                        document.removeEventListener('k3-def-registered', handler);
                        resolve();
                    }
                };
                document.addEventListener('k3-def-registered', handler);
            });
        }
        
        const use = document.createElement('k3-use');
        use.setAttribute('type', templateName);
        use.id = props.id || `k3-${templateName}-${Date.now()}`;
        
        // Apply props (CSS variables + attributes)
        for (const [key, value] of Object.entries(props)) {
            if (key.startsWith('--')) {
                use.style.setProperty(key, value);
            } else if (key !== 'id') {
                use.setAttribute(key, value);
            }
        }
        
        if (parent) {
            parent.appendChild(use);
        }
        
        // Trigger initialization
        if (use.initInstance) {
            use.initInstance();
        }
        
        K3.emit('k3:create', { id: use.id, type: templateName });
        return use;
    },
    
    /**
     * Clone an existing element
     * @param {string} sourceId - ID of element to clone
     * @param {object} props - Property overrides
     * @returns {Element} New element
     */
    clone(sourceId, props = {}) {
        const source = document.getElementById(sourceId);
        if (!source) throw new Error(`Element ${sourceId} not found`);
        
        const clone = source.cloneNode(true);
        clone.id = props.id || `${sourceId}-clone-${Date.now()}`;
        
        // Apply overrides
        for (const [key, value] of Object.entries(props)) {
            if (key !== 'id') {
                clone.setAttribute(key, value);
            }
        }
        
        return clone;
    },
    
    // === STATE MANAGEMENT ===
    
    /**
     * Get immutable snapshot of element state
     * @param {string} id - Element ID
     * @returns {object} Frozen state object
     */
    getState(id) {
        return K3._stateManager.getState(id);
    },
    
    /**
     * Restore element to a previous state
     * @param {string} id - Element ID
     * @param {object} state - State from getState()
     */
    setState(id, state) {
        K3._stateManager.setState(id, state);
    },
    
    /**
     * Update element properties (THE CORE METHOD)
     * @param {string} id - Element ID
     * @param {object} changes - Properties to change
     * @param {object} options - { immediate: boolean, force: boolean }
     * @returns {object} { success: boolean, errors: array, applied: object }
     */
    update(id, changes, options = {}) {
        return K3._updatePipeline.update(id, changes, options);
    },
    
    /**
     * Batch update multiple elements atomically
     * @param {array} updates - [{id, changes}, ...]
     * @returns {array} Results for each update
     */
    batch(updates) {
        return K3._updatePipeline.batch(updates);
    },
    
    /**
     * Force re-sync from DOM (use sparingly)
     * @param {string} id - Element ID
     */
    sync(id) {
        K3._stateManager.markDirty(id);
        K3._stateManager.getState(id); // Forces recompute
    },
    
    // === TRANSFORM HELPERS ===
    
    setPosition(id, {x, y, z}) {
        return K3.update(id, {x, y, z});
    },
    
    setRotation(id, {rx, ry, rz}) {
        return K3.update(id, {rx, ry, rz});
    },
    
    setScale(id, scale) {
        if (typeof scale === 'number') {
            return K3.update(id, {scale});
        }
        return K3.update(id, scale); // {sx, sy, sz}
    },
    
    translate(id, delta) {
        const state = K3.getState(id);
        return K3.update(id, {
            x: state.position.x + (delta.x || 0),
            y: state.position.y + (delta.y || 0),
            z: state.position.z + (delta.z || 0)
        });
    },
    
    rotate(id, delta) {
        const state = K3.getState(id);
        return K3.update(id, {
            rx: state.rotation.rx + (delta.rx || 0),
            ry: state.rotation.ry + (delta.ry || 0),
            rz: state.rotation.rz + (delta.rz || 0)
        });
    },
    
    // === QUERIES ===
    
    /**
     * Get bounding box (local + world)
     * @param {string} id - Element ID
     * @returns {object} { local: {...}, world: {...} }
     */
    getBounds(id) {
        return K3.getState(id).bounds;
    },
    
    /**
     * Get constraints from schema
     * @param {string} id - Element ID
     * @returns {object} { position: {...}, rotation: {...}, forbidden: [...] }
     */
    getConstraints(id) {
        return K3._constraintSystem.getConstraints(id);
    },
    
    /**
     * Raycast to find element at screen coordinates
     * @param {number} x - clientX
     * @param {number} y - clientY
     * @param {object} options - { mode: 'root'|'leaf', filter: fn }
     * @returns {Element|null}
     */
    pick(x, y, options = {}) {
        return K3._spatialQuery.pick(x, y, options);
    },
    
    /**
     * Query elements with filters
     * @param {string} selector - CSS selector
     * @param {object} filters - { type: string, bounds: {...} }
     * @returns {Element[]}
     */
    query(selector, filters = {}) {
        return K3._spatialQuery.query(selector, filters);
    },
    
    /**
     * Find all instances of a type
     * @param {string} type - Template name
     * @returns {Element[]}
     */
    findByType(type) {
        return Array.from(document.querySelectorAll(`k3-use[type="${type}"]`));
    },
    
    /**
     * Get K3 children of an element
     * @param {string} id - Parent element ID
     * @param {boolean} recursive - Include descendants
     * @returns {string[]} Array of child IDs
     */
    findChildren(id, recursive = false) {
        return K3._hierarchyManager.getChildren(id, recursive);
    },
    
    // === SERIALIZATION ===
    
    /**
     * Export element to HTML string
     * @param {string} id - Element ID
     * @returns {string} HTML snippet
     */
    serialize(id) {
        return K3._serializer.toHTML(id);
    },
    
    /**
     * Create element from HTML string
     * @param {string} html - HTML snippet
     * @returns {Element|Element[]}
     */
    deserialize(html) {
        return K3._serializer.fromHTML(html);
    },
    
    /**
     * Export in different formats
     * @param {string} id - Element ID
     * @param {string} format - 'html'|'json'
     * @returns {string|object}
     */
    export(id, format = 'html') {
        if (format === 'html') return K3.serialize(id);
        if (format === 'json') return K3._serializer.toJSON(id);
        throw new Error(`Unknown format: ${format}`);
    },
    
    /**
     * Import from different formats
     * @param {string|object} data - Data to import
     * @param {string} format - 'html'|'json'
     * @returns {Element|Element[]}
     */
    import(data, format = 'html') {
        if (format === 'html') return K3.deserialize(data);
        if (format === 'json') return K3._serializer.fromJSON(data);
        throw new Error(`Unknown format: ${format}`);
    },
    
    // === VALIDATION ===
    
    /**
     * Validate a value before applying
     * @param {string} id - Element ID
     * @param {string} attr - Attribute name
     * @param {any} value - Value to validate
     * @returns {object} { valid: boolean, corrected: any, error: string }
     */
    validate(id, attr, value) {
        const element = document.getElementById(id);
        return K3._validator.validate(element, attr, value);
    },
    
    /**
     * Check if element violates constraints
     * @param {string} id - Element ID
     * @returns {object} { valid: boolean, violations: array }
     */
    checkConstraints(id) {
        return K3._constraintSystem.check(id);
    },
    
    // === SIGNALS (RIGGING) ===
    
    signals: {
        emit(target, name, value) {
            const event = new CustomEvent('k3-signal', {
                detail: { name, value: Math.max(0, Math.min(1, value)) },
                bubbles: true
            });
            
            if (typeof target === 'string') {
                target = document.getElementById(target);
            }
            
            target.dispatchEvent(event);
        },
        
        register(target, name, callback) {
            if (typeof target === 'string') {
                target = document.getElementById(target);
            }
            
            const handler = (e) => {
                if (e.detail.name === name) {
                    callback(e.detail.value);
                }
            };
            
            target.addEventListener('k3-signal', handler);
            return () => target.removeEventListener('k3-signal', handler);
        },
        
        unregister(target, name) {
            // Handler returned by register() is already cleanup
        }
    },
    
    // === EVENTS ===
    
    /**
     * Listen to K3 events
     * @param {string} event - 'k3:ready'|'k3:change'|'k3:create'|'k3:error'
     * @param {function} callback - Handler
     */
    on(event, callback) {
        document.addEventListener(event, callback);
    },
    
    off(event, callback) {
        document.removeEventListener(event, callback);
    },
    
    once(event, callback) {
        const handler = (e) => {
            callback(e);
            document.removeEventListener(event, handler);
        };
        document.addEventListener(event, handler);
    },
    
    /**
     * Emit K3 event
     * @param {string} event - Event name
     * @param {object} detail - Event data
     */
    emit(event, detail) {
        document.dispatchEvent(new CustomEvent(event, { detail }));
    },
    
    // === REGISTRY (READ-ONLY) ===
    
    registry: new Proxy({}, {
        get(target, prop) {
            if (prop === 'has') {
                return (name) => window.K3._internalRegistry.has(name);
            }
            if (prop === 'get') {
                return (name) => window.K3._internalRegistry.get(name);
            }
            if (prop === 'list') {
                return () => Array.from(window.K3._internalRegistry.keys());
            }
            return undefined;
        },
        set() {
            throw new Error('K3.registry is read-only');
        }
    }),
    
    // Internal registry (not exposed)
    _internalRegistry: new Map(),
    
    // === CONFIG ===
    
    config: {
        coordSystem: 'native',  // 'native' or 'cartesian'
        maxSlices: 100,
        debug: false,
        snapToGrid: false,
        gridSize: 10
    },
    
    // === UTILS ===
    
    utils: {
        normalizeAngle(degrees) {
            let angle = degrees % 360;
            if (angle < 0) angle += 360;
            return angle;
        },
        
        lerpColor(c1, c2, t) {
            // Implementation from ColorUtils
            return ColorUtils.lerpColor(
                ColorUtils.parse(c1),
                ColorUtils.parse(c2),
                t
            );
        },
        
        degToRad(deg) {
            return deg * Math.PI / 180;
        },
        
        radToDeg(rad) {
            return rad * 180 / Math.PI;
        }
    }
};

// Initialize internal systems
K3._stateManager = new K3StateManager();
K3._updatePipeline = new K3UpdatePipeline();
K3._validator = new K3Validator();
K3._constraintSystem = new K3ConstraintSystem();
K3._spatialQuery = new K3SpatialQuery();
K3._hierarchyManager = new K3HierarchyManager();
K3._serializer = new K3Serializer();

// Freeze public surface
Object.freeze(K3.create);
Object.freeze(K3.update);
Object.freeze(K3.getState);
// ... freeze all public methods

4.2 K3StateManager (NEW - v1.0)

Location: js/engine/core/k3-state-manager.js

/**
 * K3 State Manager
 * Manages state snapshots and dirty tracking.
 */

export class K3StateManager {
    constructor() {
        this._cache = new Map(); // id -> state
        this._dirty = new Set();  // ids needing recompute
    }
    
    /**
     * Get immutable state snapshot
     * @param {string} id - Element ID
     * @returns {object} Frozen state
     */
    getState(id) {
        const element = document.getElementById(id);
        if (!element) {
            throw new Error(`Element ${id} not found`);
        }
        
        // Return cached if not dirty
        if (!this._dirty.has(id) && this._cache.has(id)) {
            return this._cache.get(id);
        }
        
        // Compute fresh state
        const state = this._computeState(element);
        this._cache.set(id, state);
        this._dirty.delete(id);
        
        return state;
    }
    
    /**
     * Restore element to state
     * @param {string} id - Element ID
     * @param {object} state - Previous state
     */
    setState(id, state) {
        const element = document.getElementById(id);
        if (!element) {
            throw new Error(`Element ${id} not found`);
        }
        
        // Apply position
        if (state.position) {
            element.setAttribute('x', state.position.x);
            element.setAttribute('y', state.position.y);
            element.setAttribute('z', state.position.z);
        }
        
        // Apply rotation
        if (state.rotation) {
            element.setAttribute('rx', state.rotation.rx);
            element.setAttribute('ry', state.rotation.ry);
            element.setAttribute('rz', state.rotation.rz);
        }
        
        // Apply scale
        if (state.scale) {
            if (state.scale.uniform !== undefined) {
                element.setAttribute('scale', state.scale.uniform);
            } else {
                element.setAttribute('sx', state.scale.sx);
                element.setAttribute('sy', state.scale.sy);
                element.setAttribute('sz', state.scale.sz);
            }
        }
        
        // Apply geometry (if changed and generative)
        if (state.geometry) {
            for (const [key, value] of Object.entries(state.geometry)) {
                element.setAttribute(key, value);
            }
        }
        
        // Apply appearance
        if (state.appearance) {
            for (const [key, value] of Object.entries(state.appearance)) {
                element.setAttribute(key, value);
            }
        }
        
        // Update transform
        if (element.updateTransform) {
            element.updateTransform();
        }
        
        this.markDirty(id);
    }
    
    /**
     * Mark element as needing recompute
     * @param {string} id - Element ID
     */
    markDirty(id) {
        this._dirty.add(id);
        
        // Mark children dirty (transforms cascade)
        const children = this._getK3Children(id);
        children.forEach(childId => this._dirty.add(childId));
    }
    
    /**
     * Compute fresh state from DOM
     * @private
     */
    _computeState(element) {
        const id = element.id;
        const tagName = element.tagName.toLowerCase();
        
        // Base state
        const state = {
            id,
            tagName,
            type: element.getAttribute('type') || null,
            
            position: {
                x: parseFloat(element.getAttribute('x') || 0),
                y: parseFloat(element.getAttribute('y') || 0),
                z: parseFloat(element.getAttribute('z') || 0)
            },
            
            rotation: {
                rx: parseFloat(element.getAttribute('rx') || 0),
                ry: parseFloat(element.getAttribute('ry') || 0),
                rz: parseFloat(element.getAttribute('rz') || 0)
            },
            
            scale: {
                sx: parseFloat(element.getAttribute('sx') || element.getAttribute('scale') || 1),
                sy: parseFloat(element.getAttribute('sy') || element.getAttribute('scale') || 1),
                sz: parseFloat(element.getAttribute('sz') || element.getAttribute('scale') || 1),
                uniform: parseFloat(element.getAttribute('scale') || 1)
            }
        };
        
        // Geometry (type-specific)
        state.geometry = this._getGeometry(element);
        
        // Appearance
        state.appearance = this._getAppearance(element);
        
        // Bounds
        state.bounds = this._computeBounds(element);
        
        // Schema (for k3-use)
        if (tagName === 'k3-use' && state.type) {
            const def = window.K3._internalRegistry.get(state.type);
            state.schema = def?.schema || null;
        }
        
        // Constraints
        state.constraints = this._getConstraints(element);
        
        // Hierarchy
        state.parent = this._getK3Parent(element);
        state.children = this._getK3Children(id);
        
        return Object.freeze(state);
    }
    
    _getGeometry(element) {
        const tagName = element.tagName.toLowerCase();
        const geo = {};
        
        switch(tagName) {
            case 'k3-plane':
            case 'k3-circle':
                geo.w = parseFloat(element.getAttribute('w') || element.getAttribute('d') || 100);
                geo.h = parseFloat(element.getAttribute('h') || element.getAttribute('d') || 100);
                geo.radius = parseFloat(element.getAttribute('radius') || 0);
                break;
            case 'k3-box':
                geo.w = parseFloat(element.getAttribute('w') || 100);
                geo.h = parseFloat(element.getAttribute('h') || 100);
                geo.d = parseFloat(element.getAttribute('d') || 100);
                geo.radius = parseFloat(element.getAttribute('radius') || 0);
                break;
            case 'k3-sphere':
                geo.radius = parseFloat(element.getAttribute('radius') || 50);
                break;
            case 'k3-stack':
                geo.depth = parseFloat(element.getAttribute('depth') || 100);
                geo.density = parseFloat(element.getAttribute('density') || 1);
                break;
            case 'k3-extrude':
                geo.depth = parseFloat(element.getAttribute('depth') || 40);
                geo.density = parseFloat(element.getAttribute('density') || 1);
                break;
        }
        
        return geo;
    }
    
    _getAppearance(element) {
        const app = {};
        
        app.fill = element.getAttribute('fill');
        app.color = element.getAttribute('color');
        app.stroke = element.getAttribute('stroke');
        app.side = element.getAttribute('side');
        
        return app;
    }
    
    _computeBounds(element) {
        // Use element's getBounds() if available
        if (element.getBounds && typeof element.getBounds === 'function') {
            const local = element.getBounds();
            
            // Compute world bounds (apply parent transforms)
            const world = this._transformBounds(local, element);
            
            return { local, world };
        }
        
        // Fallback for elements without getBounds
        return {
            local: {
                min: {x: -50, y: -50, z: 0},
                max: {x: 50, y: 50, z: 0},
                size: {w: 100, h: 100, d: 0},
                center: {x: 0, y: 0, z: 0}
            },
            world: {
                min: {x: -50, y: -50, z: 0},
                max: {x: 50, y: 50, z: 0},
                size: {w: 100, h: 100, d: 0},
                center: {x: 0, y: 0, z: 0}
            }
        };
    }
    
    _transformBounds(localBounds, element) {
        // Get element position
        const x = parseFloat(element.getAttribute('x') || 0);
        const y = parseFloat(element.getAttribute('y') || 0);
        const z = parseFloat(element.getAttribute('z') || 0);
        
        // For v1.0: simple translation (no rotation applied to bounds)
        // v2.0 TODO: Proper OBB (Oriented Bounding Box)
        
        return {
            min: {
                x: localBounds.min.x + x,
                y: localBounds.min.y + y,
                z: localBounds.min.z + z
            },
            max: {
                x: localBounds.max.x + x,
                y: localBounds.max.y + y,
                z: localBounds.max.z + z
            },
            size: localBounds.size,
            center: {
                x: localBounds.center.x + x,
                y: localBounds.center.y + y,
                z: localBounds.center.z + z
            }
        };
    }
    
    _getConstraints(element) {
        // v1.0: Basic constraints from schema
        // v2.0: Add declarative constraints (data-k3-limit-*)
        
        const constraints = {
            position: null,
            rotation: { min: {rx: 0, ry: 0, rz: 0}, max: {rx: 360, ry: 360, rz: 360} },
            forbidden: []
        };
        
        // Check for forbidden zones in schema
        if (element.tagName === 'K3-USE') {
            const type = element.getAttribute('type');
            const def = window.K3._internalRegistry.get(type);
            
            if (def?.schema?.constraints) {
                Object.assign(constraints, def.schema.constraints);
            }
        }
        
        return constraints;
    }
    
    _getK3Parent(element) {
        let parent = element.parentElement;
        
        while (parent && parent.tagName !== 'BODY') {
            const tag = parent.tagName.toLowerCase();
            if (tag.startsWith('k3-') && tag !== 'k3-scene') {
                return parent.id || null;
            }
            parent = parent.parentElement;
        }
        
        return null;
    }
    
    _getK3Children(id) {
        const element = document.getElementById(id);
        if (!element) return [];
        
        const children = [];
        const traverse = (node) => {
            for (const child of node.children) {
                const tag = child.tagName.toLowerCase();
                if (tag.startsWith('k3-') && tag !== 'k3-scene' && child.id) {
                    children.push(child.id);
                }
                // Don't traverse into k3-use (Shadow DOM boundary)
                if (tag !== 'k3-use') {
                    traverse(child);
                }
            }
        };
        
        traverse(element);
        return children;
    }
}

4.3 K3UpdatePipeline (NEW - v1.0)

Location: js/engine/core/k3-update-pipeline.js

/**
 * K3 Update Pipeline
 * Validates and applies changes to elements.
 */

export class K3UpdatePipeline {
    constructor() {
        this._batchQueue = [];
        this._batchTimer = null;
    }
    
    /**
     * Update element properties
     * @param {string} id - Element ID
     * @param {object} changes - Properties to change
     * @param {object} options - { immediate: boolean, force: boolean }
     * @returns {object} { success: boolean, errors: array, applied: object }
     */
    update(id, changes, options = {}) {
        const element = document.getElementById(id);
        if (!element) {
            return {
                success: false,
                errors: [{ type: 'not_found', id }],
                applied: {}
            };
        }
        
        const result = {
            success: true,
            errors: [],
            applied: {},
            regenerated: false
        };
        
        // Classify changes
        const { generative, visual } = this._classifyChanges(element, changes);
        
        // === STEP 1: VALIDATE ===
        for (const [attr, value] of Object.entries(changes)) {
            const validation = window.K3._validator.validate(element, attr, value);
            
            if (validation.valid) {
                if (generative[attr]) {
                    generative[attr] = value;
                } else {
                    visual[attr] = value;
                }
            } else {
                result.success = false;
                result.errors.push({
                    attr,
                    value,
                    error: validation.error,
                    corrected: validation.corrected
                });
                
                // Use corrected value if available
                if (validation.corrected !== undefined) {
                    if (generative[attr]) {
                        generative[attr] = validation.corrected;
                    } else {
                        visual[attr] = validation.corrected;
                    }
                }
                
                // Emit error event
                K3.emit('k3:error', {
                    id,
                    attr,
                    value,
                    error: validation.error
                });
            }
        }
        
        // === STEP 2: APPLY VISUAL CHANGES (fast) ===
        for (const [attr, value] of Object.entries(visual)) {
            element.setAttribute(attr, value);
            result.applied[attr] = value;
        }
        
        // === STEP 3: REGENERATE GEOMETRY (slow) ===
        if (Object.keys(generative).length > 0) {
            this._regenerateGeometry(element, generative);
            Object.assign(result.applied, generative);
            result.regenerated = true;
        }
        
        // === STEP 4: UPDATE TRANSFORM ===
        if (element.updateTransform) {
            element.updateTransform();
        }
        
        // === STEP 5: MARK DIRTY ===
        window.K3._stateManager.markDirty(id);
        
        // === STEP 6: EMIT EVENT ===
        K3.emit('k3:change', {
            id,
            changes: result.applied,
            regenerated: result.regenerated
        });
        
        return result;
    }
    
    /**
     * Batch update multiple elements
     * @param {array} updates - [{id, changes}, ...]
     * @returns {array} Results
     */
    batch(updates) {
        const results = [];
        
        // Process all updates
        for (const { id, changes, options } of updates) {
            const result = this.update(id, changes, { ...options, immediate: true });
            results.push(result);
        }
        
        // Single reflow for all
        document.body.offsetHeight; // Force reflow
        
        return results;
    }
    
    /**
     * Classify changes into generative vs visual
     * @private
     */
    _classifyChanges(element, changes) {
        const generative = {};
        const visual = {};
        
        // Get schema if available
        let schema = null;
        if (element.tagName === 'K3-USE') {
            const type = element.getAttribute('type');
            const def = window.K3._internalRegistry.get(type);
            schema = def?.schema;
        }
        
        for (const [attr, value] of Object.entries(changes)) {
            // Check if generative via schema
            const propDef = schema?.props?.[attr];
            
            if (propDef?.generative) {
                generative[attr] = value;
            } else {
                visual[attr] = value;
            }
        }
        
        return { generative, visual };
    }
    
    /**
     * Regenerate geometry for generative changes
     * @private
     */
    _regenerateGeometry(element, changes) {
        // Only k3-use can have generators
        if (element.tagName !== 'K3-USE') return;
        
        const type = element.getAttribute('type');
        const def = window.K3._internalRegistry.get(type);
        
        if (!def?.generator) return;
        
        // Merge current props with changes
        const currentProps = this._getCurrentProps(element, def.schema);
        const newProps = { ...currentProps, ...changes };
        
        // Execute generator
        try {
            const newHTML = def.generator(newProps);
            
            // Replace Shadow DOM content
            if (element.shadowRoot) {
                element.shadowRoot.innerHTML = newHTML;
            }
            
            // Re-initialize children
            if (element._initChildren) {
                element._initChildren();
            }
        } catch (err) {
            console.error('[K3] Generator error:', err);
            K3.emit('k3:error', {
                id: element.id,
                error: 'generator_failed',
                message: err.message
            });
        }
    }
    
    _getCurrentProps(element, schema) {
        const props = {};
        
        if (!schema?.props) return props;
        
        for (const [key, def] of Object.entries(schema.props)) {
            const value = element.getAttribute(key) || 
                          element.style.getPropertyValue(key) ||
                          def.default;
            props[key] = value;
        }
        
        return props;
    }
}

4.4 K3Validator (NEW - v1.0)

Location: js/engine/core/k3-validator.js

/**
 * K3 Validator
 * Validates values against constraints.
 */

export class K3Validator {
    /**
     * Validate a value
     * @param {Element} element - Element to validate
     * @param {string} attr - Attribute name
     * @param {any} value - Value to validate
     * @returns {object} { valid: boolean, corrected: any, error: string }
     */
    validate(element, attr, value) {
        // Type validation
        const typeCheck = this._validateType(attr, value);
        if (!typeCheck.valid) {
            return typeCheck;
        }
        
        // Range validation
        const rangeCheck = this._validateRange(attr, value);
        if (!rangeCheck.valid) {
            return rangeCheck;
        }
        
        // Forbidden zones (for rotation)
        const forbiddenCheck = this._validateForbidden(element, attr, value);
        if (!forbiddenCheck.valid) {
            return forbiddenCheck;
        }
        
        return { valid: true, corrected: value };
    }
    
    _validateType(attr, value) {
        // Position/rotation/scale must be numbers
        const numericAttrs = ['x', 'y', 'z', 'rx', 'ry', 'rz', 'scale', 'sx', 'sy', 'sz',
                             'w', 'h', 'd', 'radius', 'depth', 'density'];
        
        if (numericAttrs.includes(attr)) {
            const num = parseFloat(value);
            if (isNaN(num)) {
                return {
                    valid: false,
                    error: `Type mismatch: ${attr} must be a number, got "${value}"`,
                    corrected: 0
                };
            }
            return { valid: true, corrected: num };
        }
        
        return { valid: true, corrected: value };
    }
    
    _validateRange(attr, value) {
        const num = parseFloat(value);
        
        // Rotation: 0-360
        if (['rx', 'ry', 'rz'].includes(attr)) {
            if (num < 0) {
                return {
                    valid: false,
                    error: `${attr} cannot be negative (got ${num})`,
                    corrected: 0
                };
            }
            if (num > 360) {
                return {
                    valid: false,
                    error: `${attr} exceeds max 360 (got ${num})`,
                    corrected: 360
                };
            }
        }
        
        // Density: 0.1-10 (reasonable range)
        if (attr === 'density') {
            if (num < 0.1) {
                return {
                    valid: false,
                    error: `density too low (got ${num}, min 0.1)`,
                    corrected: 0.1
                };
            }
            if (num > 10) {
                return {
                    valid: false,
                    error: `density too high (got ${num}, max 10)`,
                    corrected: 10
                };
            }
        }
        
        // Scale: must be positive
        if (['scale', 'sx', 'sy', 'sz'].includes(attr)) {
            if (num <= 0) {
                return {
                    valid: false,
                    error: `${attr} must be positive (got ${num})`,
                    corrected: 1
                };
            }
        }
        
        return { valid: true };
    }
    
    _validateForbidden(element, attr, value) {
        // Check forbidden zones from constraints
        const constraints = window.K3._stateManager.getState(element.id).constraints;
        
        if (!constraints?.forbidden || !['rx', 'ry', 'rz'].includes(attr)) {
            return { valid: true };
        }
        
        const num = parseFloat(value);
        const axis = attr;
        
        for (const zone of constraints.forbidden) {
            if (zone.axis === axis) {
                const min = zone.min;
                const max = zone.max;
                
                if (num >= min && num <= max) {
                    // In forbidden zone
                    const corrected = num < (min + max) / 2 ? min - 1 : max + 1;
                    
                    return {
                        valid: false,
                        error: `${attr}=${num} is in forbidden zone [${min}, ${max}]`,
                        corrected: Math.max(0, Math.min(360, corrected))
                    };
                }
            }
        }
        
        return { valid: true };
    }
}

5. API CONTRACT v1.0

5.1 Breaking Change Policy

Tier S (Sacred - Never Break):

Tier A (1-Year Guarantee):

World bounds are approximate in v1.0.

Accuracy model is explicitly defined in
Addendum 03 — Bounds Accuracy Model

Tier B (Experimental - May Change):

5.2 Version Numbering

MAJOR.MINOR.PATCH

MAJOR: Breaking changes to Tier S (5+ year cycle)
MINOR: New features or Tier A breaking changes (1 year notice)
PATCH: Bug fixes, no breaking changes

5.3 Deprecation Timeline

Month 0: Announce deprecation, add console warnings
Month 1-11: Both old and new work
Month 12: Remove old, provide migration tool

6. TEST SUITE & ACCEPTANCE CRITERIA

6.1 Test Structure

tests/
├── unit/
│   ├── k3-api.test.js
│   ├── k3-state-manager.test.js
│   ├── k3-update-pipeline.test.js
│   ├── k3-validator.test.js
│   └── primitives/
│       ├── k3-plane.test.js
│       ├── k3-box.test.js
│       ├── k3-sphere.test.js
│       ├── k3-stack.test.js
│       └── k3-extrude.test.js
├── integration/
│   ├── lifecycle.test.js
│   ├── hierarchy.test.js
│   ├── signals.test.js
│   └── editor-api.test.js
└── e2e/
    ├── laptop-demo.test.js
    ├── molecule-demo.test.js
    └── gear-demo.test.js

6.2 Unit Tests (MUST PASS for v1.0)

File: tests/unit/k3-api.test.js

describe('K3 API - Creation', () => {
    beforeEach(() => {
        document.body.innerHTML = '<k3-scene id="scene"></k3-scene>';
    });
    
    test('K3.create() creates element with ID', () => {
        const plane = K3.create('k3-plane', { w: 100, h: 100 });
        
        expect(plane).toBeInstanceOf(HTMLElement);
        expect(plane.tagName).toBe('K3-PLANE');
        expect(plane.id).toBeDefined();
        expect(plane.getAttribute('w')).toBe('100');
    });
    
    test('K3.create() applies initial position', () => {
        const box = K3.create('k3-box', { x: 50, y: 100, z: 25 });
        
        expect(box.getAttribute('x')).toBe('50');
        expect(box.getAttribute('y')).toBe('100');
        expect(box.getAttribute('z')).toBe('25');
    });
    
    test('K3.create() auto-appends if parent provided', () => {
        const scene = document.getElementById('scene');
        const plane = K3.create('k3-plane', {}, scene);
        
        expect(plane.parentElement).toBe(scene);
    });
    
    test('K3.create() emits k3:create event', (done) => {
        K3.on('k3:create', (e) => {
            expect(e.detail.type).toBe('k3-box');
            expect(e.detail.id).toBeDefined();
            done();
        });
        
        K3.create('k3-box');
    });
});

describe('K3 API - State Management', () => {
    test('K3.getState() returns frozen snapshot', () => {
        const box = K3.create('k3-box', { x: 100, y: 50, w: 200 });
        const state = K3.getState(box.id);
        
        expect(Object.isFrozen(state)).toBe(true);
        expect(state.position.x).toBe(100);
        expect(state.position.y).toBe(50);
        expect(state.geometry.w).toBe(200);
    });
    
    test('K3.getState() includes bounds', () => {
        const box = K3.create('k3-box', { w: 100, h: 50, d: 30 });
        const state = K3.getState(box.id);
        
        expect(state.bounds).toBeDefined();
        expect(state.bounds.local).toBeDefined();
        expect(state.bounds.world).toBeDefined();
        expect(state.bounds.local.size.w).toBe(100);
    });
    
    test('K3.setState() restores state', () => {
        const plane = K3.create('k3-plane', { x: 100, y: 50 });
        const snapshot = K3.getState(plane.id);
        
        K3.update(plane.id, { x: 200, y: 100 });
        expect(plane.getAttribute('x')).toBe('200');
        
        K3.setState(plane.id, snapshot);
        expect(plane.getAttribute('x')).toBe('100');
        expect(plane.getAttribute('y')).toBe('50');
    });
});

describe('K3 API - Update', () => {
    test('K3.update() changes attributes', () => {
        const box = K3.create('k3-box', { x: 0, y: 0 });
        
        const result = K3.update(box.id, { x: 100, y: 50 });
        
        expect(result.success).toBe(true);
        expect(box.getAttribute('x')).toBe('100');
        expect(box.getAttribute('y')).toBe('50');
    });
    
    test('K3.update() validates constraints', () => {
        const plane = K3.create('k3-plane', { rx: 0 });
        
        const result = K3.update(plane.id, { rx: 400 });
        
        expect(result.success).toBe(false);
        expect(result.errors.length).toBeGreaterThan(0);
        expect(result.errors[0].attr).toBe('rx');
        expect(plane.getAttribute('rx')).toBe('360'); // Auto-corrected
    });
    
    test('K3.update() emits k3:change event', (done) => {
        const box = K3.create('k3-box');
        
        K3.on('k3:change', (e) => {
            expect(e.detail.id).toBe(box.id);
            expect(e.detail.changes.x).toBe(100);
            done();
        });
        
        K3.update(box.id, { x: 100 });
    });
    
    test('K3.update() marks state dirty', () => {
        const plane = K3.create('k3-plane', { x: 0 });
        const state1 = K3.getState(plane.id);
        
        K3.update(plane.id, { x: 100 });
        const state2 = K3.getState(plane.id);
        
        expect(state2.position.x).toBe(100);
        expect(state2).not.toBe(state1); // New snapshot
    });
});

describe('K3 API - Queries', () => {
    test('K3.getBounds() returns local and world', () => {
        const box = K3.create('k3-box', { w: 100, h: 50, d: 30, x: 10, y: 20 });
        const bounds = K3.getBounds(box.id);
        
        expect(bounds.local.size.w).toBe(100);
        expect(bounds.world.center.x).toBe(10);
        expect(bounds.world.center.y).toBe(20);
    });
    
    test('K3.findByType() finds all instances', () => {
        K3.create('k3-box');
        K3.create('k3-box');
        K3.create('k3-plane');
        
        const boxes = K3.findByType('k3-box');
        expect(boxes.length).toBe(2);
    });
    
    test('K3.findChildren() returns K3 children only', () => {
        const group = K3.create('k3-group');
        const child1 = K3.create('k3-box', {}, group);
        const child2 = K3.create('k3-plane', {}, group);
        
        const children = K3.findChildren(group.id);
        expect(children.length).toBe(2);
        expect(children).toContain(child1.id);
        expect(children).toContain(child2.id);
    });
});

describe('K3 API - Serialization', () => {
    test('K3.serialize() exports HTML', () => {
        const box = K3.create('k3-box', { x: 100, y: 50, w: 200 });
        const html = K3.serialize(box.id);
        
        expect(html).toContain('<k3-box');
        expect(html).toContain('x="100"');
        expect(html).toContain('w="200"');
    });
    
    test('K3.deserialize() recreates element', () => {
        const html = '<k3-plane x="100" y="50" w="200" h="150"></k3-plane>';
        const element = K3.deserialize(html);
        
        expect(element.tagName).toBe('K3-PLANE');
        expect(element.getAttribute('x')).toBe('100');
        expect(element.getAttribute('w')).toBe('200');
    });
    
    test('K3.export("json") creates JSON', () => {
        const box = K3.create('k3-box', { x: 100, y: 50 });
        const json = K3.export(box.id, 'json');
        
        expect(json.type).toBe('k3-box');
        expect(json.position.x).toBe(100);
        expect(json.position.y).toBe(50);
    });
});

describe('K3 API - Validation', () => {
    test('K3.validate() checks type', () => {
        const box = K3.create('k3-box');
        const result = K3.validate(box.id, 'x', 'abc');
        
        expect(result.valid).toBe(false);
        expect(result.error).toContain('number');
        expect(result.corrected).toBe(0);
    });
    
    test('K3.validate() checks range', () => {
        const plane = K3.create('k3-plane');
        const result = K3.validate(plane.id, 'rx', 400);
        
        expect(result.valid).toBe(false);
        expect(result.error).toContain('360');
        expect(result.corrected).toBe(360);
    });
    
    test('K3.checkConstraints() validates current state', () => {
        const plane = K3.create('k3-plane', { rx: 0 });
        plane.setAttribute('rx', 400); // Bypass API
        
        const check = K3.checkConstraints(plane.id);
        expect(check.valid).toBe(false);
        expect(check.violations.length).toBeGreaterThan(0);
    });
});

6.3 Integration Tests (MUST PASS for v1.0)

File: tests/integration/lifecycle.test.js

describe('K3 Lifecycle', () => {
    test('Create → Update → Serialize → Deserialize roundtrip', () => {
        // Create
        const original = K3.create('k3-box', { x: 100, y: 50, w: 200 });
        
        // Update
        K3.update(original.id, { rx: 45, ry: 90 });
        
        // Serialize
        const html = K3.serialize(original.id);
        
        // Deserialize
        const restored = K3.deserialize(html);
        
        // Verify
        expect(restored.getAttribute('x')).toBe('100');
        expect(restored.getAttribute('rx')).toBe('45');
        expect(restored.getAttribute('w')).toBe('200');
    });
    
    test('Undo/Redo with state snapshots', () => {
        const box = K3.create('k3-box', { x: 0, y: 0 });
        
        const history = [];
        K3.on('k3:change', (e) => {
            history.push(K3.getState(e.detail.id));
        });
        
        // Make changes
        K3.update(box.id, { x: 100 });
        K3.update(box.id, { x: 200 });
        K3.update(box.id, { x: 300 });
        
        // Undo
        K3.setState(box.id, history[history.length - 2]);
        expect(box.getAttribute('x')).toBe('200');
        
        // Undo again
        K3.setState(box.id, history[history.length - 3]);
        expect(box.getAttribute('x')).toBe('100');
    });
});

describe('K3 Hierarchy', () => {
    test('Parent transforms cascade to children', () => {
        const parent = K3.create('k3-group', { x: 100, y: 50 });
        const child = K3.create('k3-box', { x: 20, y: 10 }, parent);
        
        const childState = K3.getState(child.id);
        
        // Child position is local
        expect(childState.position.x).toBe(20);
        
        // World position includes parent
        // (v1.0: simplified, v2.0: proper matrix multiplication)
        expect(childState.bounds.world.center.x).toBeCloseTo(120);
    });
    
    test('Moving parent marks children dirty', () => {
        const parent = K3.create('k3-group');
        const child = K3.create('k3-box', {}, parent);
        
        K3.getState(child.id); // Cache child state
        
        K3.update(parent.id, { x: 100 });
        
        // Child state should be marked dirty
        expect(K3._stateManager._dirty.has(child.id)).toBe(true);
    });
});

describe('K3 Signals (Rigging)', () => {
    test('Signal propagates to listeners', (done) => {
        const group = K3.create('k3-group');
        group.setAttribute('k3-signal', 'test-signal');
        group.setAttribute('k3-axis', 'ry');
        group.setAttribute('k3-range', '0 90');
        
        K3.signals.register(group, 'test-signal', (value) => {
            expect(value).toBe(0.5);
            done();
        });
        
        K3.signals.emit(group, 'test-signal', 0.5);
    });
    
    test('Signal maps to transform range', () => {
        const group = K3.create('k3-group', { ry: 0 });
        group.setAttribute('k3-signal', 'rotate');
        group.setAttribute('k3-axis', 'ry');
        group.setAttribute('k3-range', '0 180');
        
        // Emit signal value 0.5 (50%)
        K3.signals.emit(group, 'rotate', 0.5);
        
        // Should map to 90° (50% of 180)
        // (Need to check after applySignal runs)
        setTimeout(() => {
            expect(group.getAttribute('ry')).toBe('90');
        }, 100);
    });
});

6.4 E2E Tests (MUST PASS for v1.0)

File: tests/e2e/laptop-demo.test.js

describe('Laptop Demo (End-to-End)', () => {
    beforeAll(async () => {
        // Load laptop.k3.html
        const response = await fetch('/assets/laptop.k3.html');
        const html = await response.text();
        document.body.innerHTML = html + '<k3-scene id="scene"></k3-scene>';
    });
    
    test('Laptop instantiates without errors', async () => {
        const laptop = await K3.instantiate('laptop', {}, document.getElementById('scene'));
        
        expect(laptop).toBeDefined();
        expect(laptop.tagName).toBe('K3-USE');
        expect(laptop.getAttribute('type')).toBe('laptop');
    });
    
    test('Laptop can rotate', () => {
        const laptop = document.querySelector('k3-use[type="laptop"]');
        
        K3.update(laptop.id, { ry: 180 });
        
        expect(laptop.getAttribute('ry')).toBe('180');
    });
    
    test('Laptop lid opens with signal', () => {
        const laptop = document.querySelector('k3-use[type="laptop"]');
        
        K3.signals.emit(laptop, 'lid-opening', 0.8);
        
        // Check that lid group rotated
        const lid = laptop.shadowRoot.querySelector('[k3-signal="lid-opening"]');
        expect(lid).toBeDefined();
        // (Exact rotation depends on k3-range in definition)
    });
    
    test('Laptop exploded view works', () => {
        const laptop = document.querySelector('k3-use[type="laptop"]');
        
        K3.signals.emit(laptop, 'explode', 1);
        
        // Check that parts moved
        // (Exact positions depend on implementation)
        const state = K3.getState(laptop.id);
        expect(state).toBeDefined();
    });
});

6.5 MILESTONE ACCEPTANCE TESTS

These tests define when v1.0 is DONE.

describe('v1.0 MILESTONE CRITERIA', () => {
    test('MILESTONE 1: Public API exists', () => {
        expect(typeof K3.create).toBe('function');
        expect(typeof K3.update).toBe('function');
        expect(typeof K3.getState).toBe('function');
        expect(typeof K3.setState).toBe('function');
        expect(typeof K3.serialize).toBe('function');
        expect(typeof K3.deserialize).toBe('function');
        expect(typeof K3.getBounds).toBe('function');
        expect(typeof K3.pick).toBe('function');
    });
    
    test('MILESTONE 2: Editor uses API (not hardcode)', () => {
        // Load editor code
        const editorCode = fs.readFileSync('./js/editor/k3-editor.js', 'utf8');
        
        // Should NOT contain direct DOM manipulation
        expect(editorCode).not.toContain('.setAttribute(');
        expect(editorCode).not.toContain('.getAttribute(');
        
        // Should contain K3 API calls
        expect(editorCode).toContain('K3.update(');
        expect(editorCode).toContain('K3.getState(');
        expect(editorCode).toContain('K3.pick(');
    });
    
    test('MILESTONE 3: State snapshots work', () => {
        const box = K3.create('k3-box', { x: 100, y: 50 });
        
        const snapshot1 = K3.getState(box.id);
        K3.update(box.id, { x: 200 });
        const snapshot2 = K3.getState(box.id);
        
        expect(snapshot1.position.x).toBe(100);
        expect(snapshot2.position.x).toBe(200);
        
        K3.setState(box.id, snapshot1);
        expect(box.getAttribute('x')).toBe('100');
    });
    
    test('MILESTONE 4: Validation prevents invalid values', () => {
        const plane = K3.create('k3-plane');
        
        const result = K3.update(plane.id, { rx: 400 });
        
        expect(result.success).toBe(false);
        expect(result.errors.length).toBeGreaterThan(0);
        expect(plane.getAttribute('rx')).toBe('360'); // Corrected
    });
    
    test('MILESTONE 5: Events are observable', () => {
        let created = false;
        let changed = false;
        
        K3.on('k3:create', () => created = true);
        K3.on('k3:change', () => changed = true);
        
        const box = K3.create('k3-box');
        K3.update(box.id, { x: 100 });
        
        expect(created).toBe(true);
        expect(changed).toBe(true);
    });
    
    test('MILESTONE 6: Serialization roundtrip is lossless', () => {
        const original = K3.create('k3-box', {
            x: 100,
            y: 50,
            z: 25,
            rx: 45,
            ry: 90,
            rz: 180,
            w: 200,
            h: 150,
            d: 100,
            color: '#ff0000'
        });
        
        const html = K3.serialize(original.id);
        const restored = K3.deserialize(html);
        
        expect(restored.getAttribute('x')).toBe('100');
        expect(restored.getAttribute('rx')).toBe('45');
        expect(restored.getAttribute('w')).toBe('200');
        expect(restored.getAttribute('color')).toBe('#ff0000');
    });
    
    test('MILESTONE 7: Generative properties regenerate geometry', async () => {
        // Create gear template with generator
        document.body.innerHTML += `
            <k3-object name="test-gear">
                <script type="application/json" data-k3-api>
                {
                    "props": {
                        "teeth": { "type": "integer", "default": 20, "generative": true }
                    }
                }
                </script>
                <script type="module" data-k3-generator>
                    export function generate({ teeth }) {
                        return \`<k3-group data-teeth="\${teeth}"></k3-group>\`;
                    }
                </script>
            </k3-object>
        `;
        
        const gear = await K3.instantiate('test-gear', { teeth: 20 });
        
        K3.update(gear.id, { teeth: 30 });
        
        const innerGroup = gear.shadowRoot.querySelector('k3-group');
        expect(innerGroup.dataset.teeth).toBe('30');
    });
});

7. KNOWN LIMITATIONS

7.1 Technical Limitations (Architectural)

No Real Shadows:

## Shadows

K3 cannot cast real shadows because CSS 3D does not provide:
- Scene depth buffer
- Shadow mapping
- Raytracing

**Alternatives**:
1. Fake shadows (blob under object)
2. Baked shadows (pre-rendered textures)
3. Canvas overlay (approximate projection)

See: docs/limitations/shadows.md

No Photorealism:

## Materials

K3 uses flat shading, not PBR:
- No metallic/roughness
- No normal maps
- No subsurface scattering

**Use Cases**:
✅ Stylized products (Lego, minimalist furniture)
✅ Technical diagrams
✅ Educational models

❌ Product photography
❌ Jewelry/luxury goods
❌ Realistic clothing

No Massive Scenes:

## Performance

K3 is limited by DOM:
- 100-500 objects: Good
- 500-1000 objects: Acceptable
- 1000+ objects: Laggy

**Mitigations**:
- LOD (hide distant objects)
- Culling (hide off-screen objects)
- Batching (group static objects)

**When to use Three.js instead**:
- Open world games
- Big data visualization
- Particle systems (10,000+ particles)

9.2 Documented Constraints

File: docs/LIMITATIONS.md

# K3 Limitations

## What K3 Can Do

✅ DOM-integrated 3D
✅ Flat shading (stylized aesthetic)
✅ 100-500 objects
✅ Simple interactions
✅ HTML content in 3D
✅ Parametric objects (gears, molecules)

## What K3 Cannot Do

❌ Photorealistic rendering (no PBR)
❌ Real shadows (CSS limitation)
❌ Deformable meshes (cloth, soft body)
❌ Massive scenes (1000+ objects)
❌ Advanced lighting (no raytracing)
❌ High-performance games (no GPU acceleration)

## When to Use K3

**Use K3 for**:
- Technical documentation with 3D
- Scientific education (molecules, physics)
- Interactive manuals
- Product customizers (stylized)
- Architecture diagrams
- Indie games (flat shading style)

**Use Three.js/Babylon for**:
- Photorealistic products
- Large open worlds
- Complex lighting
- High-performance needs
- Advanced materials

## Performance Guidelines

**Good** (< 60ms/frame):
- 100 simple objects
- 10 complex objects (stacks with many slices)
- 1-2 k3-use instances

**Acceptable** (60-100ms/frame):
- 500 simple objects
- 50 complex objects
- 5-10 k3-use instances

**Laggy** (> 100ms/frame):
- 1000+ objects
- 100+ complex objects
- 20+ k3-use instances

## Recommended Practices

1. **Use LOD**: Hide distant objects
2. **Use Culling**: Hide off-screen objects
3. **Limit Stacks**: Use density < 2 when possible
4. **Batch Static**: Group objects that don't move
5. **Profile First**: Measure before optimizing