11 KiB
ADR 0049: Penpot-Led Editor Visual System
Status
Accepted
Context
Blacksite's editor controls evolved inside individual panels. Even after the material data and Inspector composition became shared, local egui styling still produced inconsistent spacing, typography, interaction states, and narrow-panel behavior. The Penpot Inspector Material Slot provides an authored reference for the visual language rather than another implementation-specific mock-up.
Decision
The Penpot Inspector Material Slot is the source of truth for geometry, type roles, control
heights, radii, spacing, property-column geometry, and interaction states. Color is semantic rather
than panel-owned: Penpot's Assets / Colors library and editor::ui::theme::EditorVisualPalette
name the same roles, with Blacksite's existing palette as the default. A later Editor Settings slice
may select or override that palette without changing widgets or material behavior. At the 620 px
reference width, implementations match the documented geometry. Below that width, controls deliberately
reflow instead of shrinking, clipping, scrolling horizontally, or hiding required state.
Fixed Penpot sections use deterministic geometry models rather than nested egui flow layouts. Surface, parameter rows, texture fields, UV groups, and Advanced each receive an owning rectangle; every child rectangle is derived from it and clipped to it. The 569 px and 369 px section widths encode the exported wide and compact references. A bounded transient mode below 372 px keeps all rectangles finite, stacks the row, and collapses low-frequency texture actions into overflow while the Inspector restores its 420 px minimum.
The reusable implementation lives under editor::ui::design_system. It consumes the editor-owned
semantic palette and owns spacing tokens, Source Sans Pro Regular/Bold typography roles, section chrome, property grids,
asset and texture fields, segmented controls, switches, status presentation, and the Blacksite
color picker. Domain widgets consume these primitives instead of copying raw colors or absolute
Penpot coordinates.
Material authoring is the first migrated domain. One action-returning MaterialsSection is used by
primitive, static-mesh, and skinned-mesh inspectors. It owns an ordered list of collapsible
MaterialSlotPanel models, and every returned action carries its stable slot ID. The widgets do not
query or mutate arbitrary world state.
Supported material behavior only is shown. Asset identity, health, inheritance, dirty state, and
read-only state share one geometry, while stable IDs and fingerprints remain in secondary
diagnostics.
The final Penpot component supersedes the earlier v2.2/v2.3 geometry. A 32 px Materials heading precedes repeated slots. Expanded slot headers are 82 px with a 64 px preview; collapsed headers are 52 px with a 32 px preview. A cyan disclosure spine and PARAMETERS ownership label inset the Surface, Inputs, UV, and Advanced body. The asset identity block owns the single Shader selector. Penpot's former duplicate Surface "Shading Model" selector was removed from the component and every state reference in handoff v2.3.1. Surface contains blend mode, Separate/ORM selection, and Double Sided. UV precedes Advanced with an explicit 8 px section gap. Advanced render controls that do not yet have renderer contracts are shown only as a clearly disabled preview: they emit no actions and create no authored or runtime state.
The Material slot header is one assignment interaction, not an identity block beside a second drop box. Its whole 596 x 82 px wide reference area accepts valid Material/Instance drops. Preview and identity clicks plus the first-class Browse action open the compact current/recent Penpot menu; Shader, Locate, Clear, and overflow actions are isolated and cannot accidentally open it. The picker returns a stable asset reference to the owning Inspector instead of mutating scene or world state.
The custom 420 x 350 color popup is a fixed modal centered over its owning Inspector clip, with an Inspector-local dim layer rather than application- or viewport-relative placement. Its header, mode tabs, wheel and values regions, RGBA byte fields, HSV degree/percentage fields, HEX copy, checkerboard alpha control, recents, and footer use the exported Penpot coordinates. The exported eyedropper lane remains a tooltip-labelled disabled preview until screen sampling has an owned runtime contract. Apply keeps the in-memory dirty value and closes; Cancel, Escape, Close, or outside dismissal restores the exact pre-open value. Neither path saves source or schedules derived processing.
Standard Lit declares seven independently stored inputs but six primary presentation rows.
emissive_intensity is a schema companion of emissive_color; the Emissive popup previews and
restores both values as one interaction without merging their authored/runtime identities. Custom
shader binding counts likewise count primary rows while rendering companions through their owner.
Bounded scalar material inputs use the exported visible-child geometry rather than the narrower transparent Penpot parent frame. At the 620 px reference that is a 138.8 px group composed from a 6 px leading inset, 93.8 px track, 6 px gap, and 33 x 22 numeric field. At 420 px the track expands to 160.3 px while the number remains fixed. Expanding the 178 px Advanced preview requests a bounded Inspector scroll reveal so its note and lower boundary are visible. The same no-implicit-persistence rule applies to sliders, texture assignments, and shader controls.
Focused visual iteration uses a standalone ui_gallery binary that links the same editor_ui and
material_schema production crates as the editor. It owns no project, scene, renderer, or gameplay
runtime. Exact reference presets, direct Penpot PNG comparison, runtime fixture/theme reload, and a
managed last-good rebuild/restart loop shorten the edit cycle without creating a second widget
implementation. Gallery evidence is diagnostic until the same component passes a named full-editor
interaction scenario.
Renderer comparison uses the same rule. The renderer array, renderer identity, property heading,
indexed material heading, and material slots are production editor_ui components. A material
slot's asset header and parameter disclosure are related but distinct presentation state: renderer
arrays may keep the complete asset/action header visible while the expensive parameter body is
collapsed. Empty assignments use a first-class empty presentation instead of reusing pending or
failed thumbnail state.
Inspector overflow is part of the shared visual contract. A floating scrollbar keeps component width stable as content crosses the vertical overflow threshold, and every nested component clip is an intersection with the owning Inspector body. Ready texture fields use the typed thumbnail as their sole leading identity; generic image glyphs are reserved for empty states.
Standard Lit exposes one shared UV Offset/Tiling transform through the same schema-driven input document. The renderer maps it to Bevy StandardMaterial UV transforms and to every Surface ABI texture lane. Material Instances may override it sparsely; resetting an instance restores its base.
Penpot's CODEGEN MAP and handoff annotations are design documentation. Runtime egui layout remains
responsive and semantic; generated SVG or code is not copied into production. Reviewed numeric
geometry is represented by small testable layout models instead of ad hoc child flow or generated
widget trees.
The same release slice applies the active Inspector Header, Array Header, renderer-panel, asset
field, vector, selection, and overlay components across the Inspector. Built-in component cards
dispatch through the registry and share one clipped actor-body scroll owner; domain behavior stays
outside the visual primitives.
The Content Browser is the next migrated workspace. Its Penpot reference deliberately includes a
36 px standalone title row, but the production dock tab already owns that identity; docked content
therefore renders the remaining 55 px single-row toolbar at wide widths rather than duplicating
chrome. Compact composition uses the exported 88 px two-row toolbar, starts with the content grid,
and exposes mutually exclusive Sources and Details drawers from explicit actions. Wide composition
uses fixed 216 px Sources and a 290 px default resizable Details pane around a flexible content
lane. The 56 px content heading precedes one continuous row-major card stream in which folders and
assets share the same bounded card footprint. Toolbar, breadcrumb, content header, cards, Details
identity, and the catalog/processing/selection footer consume the same semantic tokens and return
actions to the owning editor module. Long identity and dependency paths use the exported bounded
two-line Details geometry rather than unbounded egui wrapping or a single unreadable clipped line.
Those presentation functions live together in
editor_ui::content_browser and are invoked by both the production panel and the UI Gallery;
editor::ui::asset_browser remains the sole owner of project queries, selection, transactions,
thumbnail scheduling, and action resolution. A fixture-only Gallery body is not valid evidence for
production conformance.
The 1240 px toolbar remains the standard-wide reference. A separate 1600 px expanded reference exposes sort and thumbnail scale in the toolbar at the 1400 px production breakpoint; standard and compact layouts retain those controls in overflow. Card scaling preserves a fixed 40 px identity footer and changes only the preview footprint, with square cached artifacts aspect-fitted rather than stretched. Sorting and scaling are presentation operations and never mutate authored content.
Consequences
- New editor UI has one reviewed visual vocabulary rather than panel-local approximations.
- Standalone design/gallery boards and docked production panels share one component without duplicating dock-owned title chrome.
- Theme colors have one runtime owner and one design-side asset vocabulary; the material panel does not carry a private blue/teal palette.
- The 620 px and 420 px geometries are deterministic and unit-testable. The Inspector has a 420 px floor and one actor-body scroll region; material slots never introduce nested scrolling.
- Unsupported prototype controls are omitted until their runtime contracts exist.
- The explicitly approved Advanced preview is the sole exception: it is disabled, muted, and labelled as planned work rather than functioning authoring state.
- Native acceptance compares rendered geometry, typography, states, and interaction behavior with the Penpot reference; compile-only evidence is insufficient.
- Source Sans Pro and its SIL Open Font License are bundled with the editor.
- ADR 0048 continues to own composition and module-size policy. This ADR owns visual tokens, responsive component geometry, and interaction presentation.