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.
- Ricardo Oliva Alonso
- Ben Evans
- Josetxu
- Tamara Littlewood
- Jhey Tompkins
- Alvaro Montoro
- Ana Tudor
- S. Shahriar
- Sarah Fossheim
- Temani Afif
- Adam Kuhn
- Julia Miocene
- Burank Can
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
- Vision & Philosophy
- Current State Analysis
- Architecture Overview
- Component Specifications
- API Contract v1.0
- Test Suite & Acceptance Criteria
- 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:
- DOM-first: 3D objects are HTML elements, not canvas textures
- Declarative: State is described, not imperatively constructed
- Honest: Limitations are documented, not hidden
- Educational: The code teaches, not just works
1.2 What K3 Is NOT
- ❌ A photorealistic renderer (no PBR, no raytracing)
- ❌ A game engine for AAA titles (no shadows, no 10k+ objects)
- ❌ A WebGL wrapper (pure CSS 3D)
- ❌ A quick hack (this is published, versioned, maintained)
1.3 Target Audience
Primary:
- Frontend developers needing 3D in documents
- Educators creating interactive STEM content
- Technical writers building interactive manuals
Secondary:
- Designers prototyping 3D interfaces
- Agencies building product customizers (stylized)
- Indie game devs (flat-shaded, Monument Valley style)
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:
K3Element(base class with TRS)K3Scene(perspective container)K3Group(transform hierarchy)K3Definition(template registry)K3Use(instantiation with Shadow DOM)
✅ Primitives:
K3Plane(single face)K3Box(6 faces with rounded corners)K3Circle(billboard circle)K3Sphere(2.5D impostor with texture mapping)K3Stack(morphing extrusion)K3Extrude(linear projection for SVG/text)
✅ Systems:
- TRS transform system (Translate-Rotate-Scale order)
- Signal bus (rigging with k3-signal/k3-axis/k3-range)
- Glare system (procedural shading)
- Basic interaction (camera rotation)
✅ Editor (HARDCODED):
- Selection logic (root vs leaf mode)
- Gizmo sync (visual bounds)
- Inspector UI (property sliders)
- ❌ PROBLEM: Direct DOM manipulation, no API usage
2.2 What is BROKEN/MISSING
❌ No Public API:
// These don't exist:
K3.create()
K3.update()
K3.getState()
K3.serialize()
❌ No MutationObserver:
- Attribute changes don't trigger re-render
- DOM is "source of truth" only on load
❌ No Error Handling:
- Invalid attributes fail silently
- No validation against schema constraints
- No
k3:errorevents
❌ 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:
- Can't serialize current state
- No undo/redo foundation
❌ Editor is Anti-Pattern:
- Shows what NOT to do
- Can't be used as example code
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):
<k3-scene>,<k3-group>,<k3-object>,<k3-use><k3-plane>,<k3-box>- Basic TRS attributes:
x, y, z, rx, ry, rz, scale K3.create(),K3.update(),K3.getState()
Tier A (1-Year Guarantee):
<k3-sphere>,<k3-stack>,<k3-extrude>- Signal system
- Advanced queries
World bounds are approximate in v1.0.
Accuracy model is explicitly defined in
Addendum 03 — Bounds Accuracy Model
Tier B (Experimental - May Change):
- Nothing in v1.0 (all stable)
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