Appearance
Edit Drags
Started: Feb 3, 2026 | Completed: Feb 4, 2026 | Status: done
Goal
Make selected geometry editable by dragging. only faces can be selected. drag only works on a corner or an edge of the selected face. editing is geometrically confined to the 2D plane of the selected face.
Work Remaining
- confine ALL rotations to one axis -> the normal of the selected face
- better edge/corner dragging: default to the selected SO
- it now defaults to the root SO
- rotate a child -> it changes size — removed
recompute_max_boundscall fromrotate_objectinEvents_3D.ts. To restore: afterquat.normalize(obj.so.orientation, obj.so.orientation);addif (obj.so.fixed && obj.parent) { orientation.recompute_max_bounds(obj.so); }
- red dots show for ENABLED drag corners, should be blue
- ability to drag the current SO
- click on a face and drag it
- ignore drag when applied to the root SO
- constrain movement within the plane of the forward face OF THE PARENT SO
- movement does not follow mouse
- need to project mouse movement (2d vector) onto this plane (different vector)
- compute the new x,y,z
- stairs occlusion shape should shrink to fit stairs
Our Work
Understand current state
- How does rotation drag work now? —
e3.set_drag_handlerin Setup.ts always callsrotate_object - What's the relationship between screen delta and world delta? — camera at (0,0,5), right=(1,0,0), up=(0,1,0)
- Where do the cube vertices live? — O_Scene.vertices, but both cubes shared same array (fixed)
Design
- Should edit drag modify O_Scene vertices directly, or go through SO? — through SO (move_vertex, move_vertices)
- How to detect "drag on selection" vs "drag to rotate"? —
edit_selectionreturns true if selection exists - What happens to inner cube when outer cube's corner moves? — nothing, they have separate vertex arrays now
Implement
- Clone vertices on Scene.create (so each object has its own)
- Add Point3.clone() method
- Add move_vertex, move_vertices, edge_vertices, face_vertices to Smart_Object
- Add screen_to_world, edit_selection to Events_3D
- Route drag: selection ? edit : rotate (Setup.ts)
- eliminate selection of edge or corner
- click-drag only applies to an edge or a corner
- drag is geometrically confined to the 2D plane of the selected face
Test manually
- Click corner, drag — resizes 2 dimensions
- Click edge, drag — resizes 1 dimension
- Click face interior — selects face (no drag)
- Click empty, drag — rotates as before
- Hover disabled during drag/rotation
Polish
- Visual feedback during drag
Artifacts
src/lib/ts/runtime/Smart_Object.ts— bounds-based geometry (x/y/z_min/max), derived vertices, face_normal, corner_in_face, edge_in_face, vertex_bound, edge_bound, axis_vectorsrc/lib/ts/signals/Events_3D.ts— ray-plane intersection, edit_selection, drag_target tracking, hover clearingsrc/lib/ts/render/Setup.ts— route drag by selectionsrc/lib/md/builds.md— build 7 entry
Key Decisions
- Bounds-based geometry: SO stores 6 bounds instead of 8 vertices. Simpler, supports negative dimensions.
- Vertices derived:
get vertices()computes from bounds using Math.min/max for consistent topology. - Ray-plane projection: Mouse positions projected onto face plane to get world-space delta.
- Bound ownership:
vertex_bound()maps vertex index to named bound it controls (for drag application).
Invisible Root
turn root into a pure container — invisible by default, non-interactive in 3D, selectable via panel or background click.
decisions
- no auto-sizing. tried it, hated it. root keeps its user-set dimensions.
- manual "fit" button in the actions row. shrink_to_fit: snapshot children's absolute bounds, resize parent to their bounding box, normalize children so offsets stay non-negative.
- fit-normalize on startup if any SO has negative start/end/length (unless root is a repeater).
- root is selectable via the parts hierarchy and via background click. attributes table shows, angles table hidden.
- root visibility persists. serialize always writes
visible. Engine only forcesvisible = falsefor brand new scenes. - background click selects root. second background click deselects.
implemented
phase 1: root invisible (rendering)
- Engine.ts —
if (!saved) root_so.visible = false(new scenes only) - Engine.ts — startup fit-normalize when negatives detected (skip if root is repeater)
- R_Dimensions.ts — skip root:
if (!obj.parent) continue - Smart_Object.ts — serialize always writes
visible(not just when false)
phase 2: root non-interactive in 3D (hit testing)
- Hits_3D.ts — face hit loop skips root:
if (!so.scene.parent) continue - Events_3D.ts — background click selects root (or deselects if root already selected)
phase 3: back face guidance
- Hits_3D.ts —
back_most_face(so):front_most_face(so) ^ 1 - Drag.ts — when parent is root, use
back_most_faceinstead offront_most_face
phase 4: fit button
- Engine.ts —
shrink_to_fit(): snapshot children's absolute bounds, compute bounding box, resize parent, normalize children offsets. disabled for repeaters. - D_Selected_Part.svelte — "fit" button in actions row, disabled when no children or is repeater.
phase 5: details panel
- D_Attributes.svelte — angles table wrapped in
{#if !is_root}(hidden for root) - D_Selected_Part.svelte — deselected root shows empty panel; disabled buttons use
{#if}notdisabled;needs_fitderived state for fit button visibility
phase 6: root_so source of truth
- Scenes.ts —
root_so: Smart_Object | null = null(plain property, SOT) - Stores.ts — removed
w_root_sowritable store androot_sogetter entirely - Engine.ts, Render.ts, R_Grid.ts, Events_3D.ts, Graph.svelte — all read
scenes.root_so
phase 7: HMR fix
- Events.ts —
private wired = falseguard prevents double document listener registration on HMR re-mount
contextual selectability
when a saved file is inserted, its root becomes a child. insert_child_from_text sets parent, so !so.scene.parent checks already treat it as non-root. SO stays invisible in both contexts. no code changes needed.
checklist of work on invisible root
- work
- root invisible by default (new scenes only), geometry retained for grid/constraints
- root non-interactive in 3D (raycaster skips), selectable via parts list or background click
- back face guidance during child drag
- manual "fit" button (snapshot children bounds, resize parent, normalize offsets)
- startup fit-normalize when negatives detected (skip if repeater)
- background click: first selects root, second deselects
- moved root_so SOT from Stores to Scenes (plain property, not a store)
- HMR double-registration fix (Events.ts wired guard)
Shadows
- back-face grid for manual testing of edit and shrink to fit
- faint solid grid on back-facing root faces (R_Grid.ts
render_back_grid) - fixed
render_gridto useroot_so.scenedirectly (invisible root brokeobjects.find) - grid spacing must NOT auto adjust with tumble (it still does)
- selected SO needs a visible "shadow" rectangle projected 2D onto each back grid face
- must be able to project a rotated SO
- whisper -> 0.12 fill, 0.5 stroke
- unrelocate the red and blue selection dots back to the frontmost SO face
- rotated SOs cast tilted shadows — can't just use axis-aligned bounds. grab the SO's 8 world-space vertices, flatten each one onto the back face (like shining a light straight through). no clipping — if the child overshoots the root wall mid-drag, the shadow should show that. project to screen, fill the convex hull with a whisper of accent color (
rgba(accent, 0.06)), thin stroke at0.15. drawn insiderender_back_grid's per-face loop, right after grid lines. skip if the root or nothing is selected.
- add a slider for grid opacity in preferences details
proposal — shadow projection
all in R_Grid.ts. no other files change.
once per frame (before face loop): get the selected SO via hits_3d.selection?.so. skip if null or root. compute child_to_root = inv(root_world) * child_world — transforms child vertices into root's local space where face planes are axis-aligned. transform all 8 vertices from sel_so.vertices through this matrix. extract accent RGB via parseToRgba from color2k.
per back-facing face (after grid lines): copy each root-local vertex, set its fixed-axis component to fixed_val — flattens it onto the face plane. project to screen via host.project_vertex(flattened, root_world). skip behind-camera points (w < 0). Graham scan convex hull on the screen points. fill rgba(r,g,b,0.06), stroke rgba(r,g,b,0.15) at 1px.
new helper: convex_hull_2d — Graham scan, lives in the helpers section alongside axis_bounds and grid_spacing. ~15 lines. only used here.
why child_to_root: can't just flatten raw bounds — a rotated child's corners aren't where its bounds say. inverting root's world matrix and multiplying by child's gives the actual corner positions in root-local space, where flattening one axis is geometrically meaningful.