Developer · The parametric system
Developer Docs

One catalog entry → a mesh, a dieline, one state

The heart of PacFully is a pipeline with no derived copies: a template definition feeds two pure generators, and both draw from the same editor state in the same render pass. This page is the map an agent needs before touching geometry.

TEMPLATE_CATALOG is the single source of truth

Each of the 11 templates is a ParametricTemplate in packages/shared/src/catalog.ts: an id, display name, category, kind (box, bottle, jar, can, pouch, tube), default dimensions in mm, the allowed materials and finishes, a dielineAvailable flag, and a description. The same entries drive the gallery cards, both generators, and the api's /templates responses — add a template to the catalog and it appears everywhere.

The 3D side: lib/models

generateModel(templateId, dims, thicknessMm) dispatches to one generator per template and returns { group, panels, animatables, size }. Conventions:

  • Millimetres in, scene units out. 1 mm equals 0.01 scene units; generators convert at the boundary.
  • Boxes are assembled per panel. Each face is a thin BoxGeometry mesh with its own material slot — that is what makes per-panel artwork and colors possible. The printable face uses a dedicated material index.
  • Containers are lathed. Bottles, jars, cans, and tubes come from LatheGeometry profiles; the pouch is an extruded millimetre shape with a bevel, scaled once.
  • Animation is declarative. Lids, flaps, and trays register as { object, channel, closed, open } animatables; a per-frame lerp moves them between poses, so no model hand-animates anything.

PANEL_REGISTRY names the printable panels per template (boxes get six faces; containers get body + cap/lid/top; pillow and pouch get front/back). It feeds the panel pickers in the UI and the artwork targeting in the store.

The 2D side: lib/dieline

Six generators — one per box style — return a DielineDoc: cut and crease paths in millimetres (y-up), dimension annotations, and PanelRegions that map blank regions back to 3D panel ids (that mapping is what powers the split-view highlight when you click either view). Bleed is 3 mm, computed with a winding-aware offset. finalizeDoc normalizes every document to a margin origin with all-positive coordinates. Three emitters render the same document: themed SVG, minimal valid R12 DXF on CUT/CREASE/BLEED/DIMS layers, and a print sheet for browser-to-PDF export.

One store, two pure views

A single zustand store holds the EditorState — template, dimensions, material, thickness, finish, background, lighting, artwork placements. There is no derived-state duplication; both views are pure functions of the same snapshot:

2D ↔ 3D sync — same inputs, same commit
PropertiesPanel ──setDimensions──▶ useEditorStore.snapshot.state.dimensions
                                        │
              ┌─────────────────────────┴─────────────────────────┐
              ▼                                                    ▼
  PackagingModel: useMemo(() =>            DielineView: useMemo(() =>
    generateModel(templateId, dims, t),      generateDieline(templateId, dims, t),
    [templateId, dims, thicknessMm])         [templateId, dims, thicknessMm])

Editing any structural input re-runs both memos in the same commit, so the mesh and the dieline can never show different geometry. Panel selection is shared the same way, and undo/redo snapshots the whole document — dimensions, colors, and artwork restore atomically (50 entries, rapid edits from one control coalesced).

Serialization and share links

Snapshots serialize to base64url JSON with clamping and enum validation on decode — hostile or corrupted ?state= parameters fall back to safe values instead of crashing the editor. encodeForShare strips artwork data URLs above 1500 characters so links stay shareable, and reports what it dropped so the toolbar can warn.

Materials pipeline

resolveLook(material, finish) starts from a per-material base look (color, roughness, metalness, clearcoat, transmission, IOR) and applies the finish as a remap — gloss drops roughness and raises clearcoat, soft-touch pushes roughness into a velvety band, matte frosts glass. The result feeds MeshPhysicalMaterial directly, with lighting and background presets handled by the scene rig.

Where the api fits

The api serves the same catalog verbatim at /templates and /v1/templates and stores editor documents as Project.artworkJson — an EditorState as JSON. When a project is created without one, the api synthesizes a default state from the template (first material and finish, studio lighting, no artwork), and dimension fields fall back in the order: explicit body values → dimensions inside the artwork JSON → template defaults.

Continue to contributing