Appearance
PiVo Ortho module contract
Browser modeling app with its own namespace, branding and fixtures. Implementation contracts below. All files live here.
Core implement src/core/index.ts and tests/core.test.ts; DO NOT edit types.ts. IO implement src/io/index.ts plus helpers and tests/io.test.ts. UI implement src/ui/* and src/main.tsx; do not edit core/IO. Orchestrator owns bootstrap/docs/types/config and integration.
Required core exports
All mutating operations edit supplied object and return void unless stated. Throw useful errors for invalid input, never allow dangling face references. Honor mesh.locked in editing operations. UI honors folder locks as well.
- uid(prefix='id'):string; createProject(name='Untitled'):Project (empty meshes, built-in 16-color palette, authored indexed checker/colored regions texture); createPrimitive(type:Primitive,name?:string):Mesh; cloneMesh(mesh:Mesh):Mesh (new IDs including face/vertices, small offset).
- translateVertices(mesh,ids:string[],delta:Vec3,snap?:number):void; mergeVertices(mesh,ids:string[]):void; deleteVertices(mesh,ids:string[]):void; deleteFace(mesh,faceId:string):void; extrudeFace(mesh,faceId:string,distance:number):void; mirrorMesh(mesh,axis:Axis):void; mergeMeshes(meshes:Mesh[],name?:string):Mesh (world transform baked and attributes preserved).
- worldVertex(mesh:Mesh,position:Vec3,pose?:Pose):Vec3; triangulateFace(mesh:Mesh,face:Face):number[] (indices into face corner list, robust concave polygon ear clipping); transformUV(mesh,faceIds:string[],edit:UVEdit):void.
- paint(texture:Texture,tool:RasterTool,from:Vec2,to:Vec2,color:number,filled?:boolean):void; floodFill(texture,x:number,y:number,color:number):void; textureRGBA(texture:Texture):Uint8Array; quantizeRGBA(rgba:Uint8Array,width:number,height:number,texture:Texture):void; swapPalette(texture,a:number,b:number):void; generateRamps(texture):void; marqueeCopy(texture,x:number,y:number,w:number,h:number):{width:number,height:number,pixels:number[]}; marqueePaste(texture,data:{width:number,height:number,pixels:number[]},x:number,y:number):void.
- createClip(meshId:string,type:ClipType,track?:number):Clip; evaluateMesh(mesh:Mesh,animation:Animation,time:number):Pose (input never mutated; no implicit looping); serializeProject(project:Project):string; parseProject(text:string):Project (strict schema, resource limits, no dangerous keys).
- class History: constructor(project:Project,limit=50); current:Project; canUndo:boolean; canRedo:boolean; undoLabel:string; redoLabel:string; commit(label:string,mutate:(draft:Project)=>void):Project (clone draft, validate, commit only on success); undo():Project; redo():Project; reset(project:Project):void. Snapshot texture is ordinary number array (serializable). Limit protects memory. One drag/paint stroke is one history commit, no per-mousemove commits.
Animation design decisions
Four numbered tracks 0–3 per mesh. duration in seconds, fps default 12. rotation clip amount in radians, movement world offset, scale amount as ending multiplier. Base transforms + additive movement/rotation and multiplicative scale evaluated in track then start then ID order. For ordinary clips progress is clamped before/after duration; before start contributes zero (scale contributes 1). Ping-pong uses triangular progress. Oscillation sine uses frequency cycles/sec within active interval, zero outside; property selects movement/rotation/scale. Visibility hides only during its active interval. No automatic loop in core; UI/export control looping. Reject invalid track/duration/time/amount. Folder visibility is applied by renderer/export, not evaluateMesh.
Required IO exports
- exportOBJ(project:Project,time?:number):ExportBundle (OBJ/MTL/texture PNG, bake current pose; hidden mesh/folder skipped; per-corner UV; material properties preserved where representable).
- exportGLTF(project:Project,options?:{animation?:boolean}):ExportBundle (glTF2/.bin/texture PNG, independent valid packing; material colors/texture/doubleSided; triangulation; bake supported motion to node TRS at fps; warnings for visibility/drawBehind/retro shader not representable).
- encodePNG(width:number,height:number,rgba:Uint8Array):Uint8Array (portable via installed fflate; do not require DOM).
- encodeGIF(frames:Uint8Array[],width:number,height:number,texture:Texture,fps:number):Uint8Array (RGBA frames; pure deterministic GIF through gifenc; preserve transparency and encode real frames; warnings if quantization).
- spriteSheet(frames:Uint8Array[],width:number,height:number,columns:number):{width:number,height:number,rgba:Uint8Array}.
- downloadBundle(bundle:ExportBundle,baseName:string):void (ZIP via fflate for multi file; Blob downloads; sanitized file names); downloadFile(file:ExportFile):void. IO can import {worldVertex,triangulateFace,evaluateMesh,textureRGBA} from ../core and shared types. May write zip via fflate. Core no IO dependency.
UI expectations
React 19 + Three.js using types/core/IO above. Deliver original crisp technical interface, dark ink workspace and warm amber controls, not PicoCAD trade dress. Use local CSS, no external fonts/network. Header Model/Paint/Animate tabs + Save/Open/Exports, real four view modeling workspace, left tools/outliner, right inspector, bottom status. 128x128 pixel canvas and UV handles/live model paint view; clip library/four lane timeline with scrubbing. Rendering must handle winding, depth, transparency, shade ramps and affine mapping. Picking should expose mesh/face/vertex modes with Shift+multiple, rectangle selection; actual dragging/snapping/transform controls. Every button works; no placeholder controls. Use own local SVG/icon markup. History is React state source; mutate only via commits or a drag preview cloned from history.current then commit once. Autosave debounced IndexedDB with safe parsing and quota handling; clear/replace recovery explicitly, no silent deletion. File input .pvo native project. Authored demonstration scene visible on first launch. Face/UV/clip inspector label all fields. Keyboard shortcuts dialog and browser errors visible. Exports use IO codecs; capture deterministic frames from renderer at chosen camera yaw/time and call encodeGIF/spriteSheet; transparent render settings, frame size, columns etc. Show export progress and cap resource choices. Browser target desktop; input types have accessible labels. Scope gaps honestly in docs, never fake a capability. Use 16-color indexed textures and current shadeRamps when shading, affine effect via a dedicated shader if feasible. No CDN/runtime network.