Skip to content

[feat] drag to place from the Explorer (roadmap 39 P1-P6) - #347

Merged
AlexZ005 merged 7 commits into
feat/39from
feat/39-place
Oct 8, 2026
Merged

AlexZ005 merged 7 commits into
feat/39from
feat/39-place

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Roadmap 39 (lane 39-place): drag to place from the Explorer, P1-P6 (VR radial placement deferred to a follow-up).

What it does

  • Drag a pack item / library model / prefab from the Explorer onto the viewport: a ghost shows where it lands
    (resting ON the surface under the pointer, grid snap unless Shift) and how big it is (W × D × H in metres +
    tris + download size). R / Shift+R / the wheel turn it 15°; Alt = the camera's focus point (dropped
    onto the surface below it); snapTargets.alignNormal tilts it onto the surface; a multi-selection drops as a row.
  • Release over the viewport = spawn at the ghost: one undo step, replicated like any add. A static pack item
    spawns as a pack stub (30c reference) drawn in the Placeholder Style until it loads; every peer refills it.
    Animated items, library models and prefabs take their import paths at the ghost pose.
  • Esc, releasing anywhere else, or dropping back on the Explorer = nothing spawns, no undo entry.
  • Ghost tiers: real model (shared geometry, one translucent material, no shadows) when decoded and allowed by
    Settings ▸ Scene ▸ Performance ▸ Placement preview (Full / Within budget (default, 50k; 15k phone/Quest) / Box
    only); otherwise the box. Hovering the viewport prefetches. Amber = mostly inside another object (still places),
    red = no surface (release does nothing).
  • Touch: long-press a card, the Explorer tucks away, drag, lift to place.
  • Pack downloads: a CacheStorage cache (packCache.js) — Downloaded badge + size on pack cards, a
    Downloaded filter in the Packs, item menu Delete cache (size) (the item stays in the pack), pack row
    Delete downloads, Storage Pack downloads + Clear all pack downloads.
  • Pack rows carry size [w,h,d] / box / tris / bytes / animated (packs PR); missing → measured on first
    load and kept in idb; library models record their dims at import. PACKS.md documented.

Design note: a pointer drag with an HTML5 bridge

A native drag delivers no keydown/wheel, so R/wheel/Esc cannot work on it. A trusted dragstart on a placeable card
is cancelled and placeDrag.js runs the gesture; over any non-viewport element it re-dispatches the card's
application/x-explorer-item payload as synthetic dragenter/over/leave/drop (+ dragend on the source), flagged
application/x-tp-place-bridge, which App's window drop ignores. Every existing HTML5 target keeps working
(Explorer folders/packs/bin, Inspector texture slot, LOD panel, pickers). Synthetic dragstarts (suites) keep the
native path.

Proof

  • place-drag 39 (two peers, real mouse): row dims, model swap, rotation, Esc / back-to-Explorer = no spawn + no
    undo, one-undo drop at the exact pose, stub replicated to B and refilled there, undo reaches B, release over chrome
    places nothing (counterfactual: 0 → 1 with the App guard removed), bridge moves a card into a folder.
  • place-drag-cache 33: downloads, badge, filter, Delete cache (bytes gone, item stays), Storage clear-all, preview
    tiers, the Settings rows, touch long-press at 390×844 (counterfactual: without the touchmove block every
    finger drag ended in pointercancel).
  • place-drag-extras 15: multi-select row as one undo, Alt focus on the ground, red over the sky.
  • place-drag-perf (exclusive): a cached 200,340-tri item with the default budget → box ghost, frame time
    16.67 ms dragging vs 16.67 ms idle, no long tasks (headless rAF is vsync-capped).
  • vitest +13 (1885 total), svelte-check 300/41 (= baseline), check:tokens clean.
  • Regression battery: see the lane handover for the A/B table.

Shots: ~/.code/lanes-30/after-39/39-place/NN-*-{before,after}.png; docs fragment beside them.

🤖 Generated with Claude Code

AlexZ005 and others added 7 commits October 8, 2026 05:55
- placementDims.js (leaf): one Dims shape {size [w,h,d], box, tris, bytes, animated, known,
  source}; read from a pack row / manifest item (a NUMERIC `size` is the old manifest's byte
  count), else what this device measured on first load (ONE idb record `pack:dims-v1`, writes
  debounced), else the 1 m "size unknown" box. Labels W x D x H + cost.
- packs.js normalizes `dims` onto every default-pack item and imported manifest item.
- packRefs.js: loadPackTemplate measures + remembers dims for a row that had none (also for an
  animated file, before it refuses to reference it — AnimatedRefError), keeps the FINISHED
  templates for a sync peek (peekPackTemplate = "decoded in memory"), and reads/writes the
  pack cache in fetchWithProgress and the LOD file fetch.
- packCache.js (leaf): ONE CacheStorage cache `tp-pack-files-v1` with a size index store, so
  downloaded items can be listed, sized and deleted; degrades to plain fetch without caches.
- explorer.js: a new library model records `dims` from the same parse its thumbnail makes.
- static/library/default/default.json: the bundled starter rows carry dims (copied from the
  packs repo's measured rows; identical files).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ep spawn (39 P1 + P2)

- placeDrag.js: a POINTER gesture (a native drag delivers no keydown/wheel, so R / the wheel /
  Esc could not work on it). Explorer cancels a TRUSTED dragstart on a placeable card (pack
  object, library model, prefab) and hands over; a touch long-press does the same with the
  Explorer collapsed. Over any non-viewport element the payload is re-dispatched as synthetic
  dragenter/over/leave/drop (+ dragend on the source), flagged x-tp-place-bridge, so every
  existing HTML5 target keeps working and App's window drop ignores it.
  Raycast onto surfaces (ground fallback), the box RESTS on the surface along its normal,
  grid snap on the surface's two axes (Shift = free), R / Shift+R / wheel = 15° steps,
  align-to-surface via snapTargets.alignNormal, Alt = the camera focus point, a row for a
  multi-selection, amber over 50% inside another object, red (no drop) off the world.
- placeGhost.js: scene-root, never saved/sent/picked. BOX tier (wire box + fill + footprint)
  first; the MODEL tier (the template's own geometry, one translucent material, no shadows)
  swaps in on a later frame when decoded + allowed by the preview setting (placementPrefs.js:
  full / within budget (default) / box, 50k tris, 15k on phones and headsets).
  Pending boxes hold a drop's spot while an import path parses.
- explorerDrop.placeFromGhost: a STATIC pack item spawns as a pack STUB through addImported
  (one create entry, the stub element on the wire, Placeholder Style until it fills, peers
  refill with derived uuids); animated / behaviour / over-budget items, library models and
  prefabs take their import paths at the ghost pose (importFile gains `quaternion`). Several
  items share one history batch. Hovering the viewport prefetches (template parse / cache /
  library or prefab decode).
- PlaceDragOverlay.svelte (kit tokens + Icon): the chip, W x D x H / size unknown, tris and
  size, the turn and the keys.
- the two older pack fetches go through the pack cache; importGate.wouldAskFor; debug rows.
- suite place-drag (33, two peers, real mouse, the CDN path served from static/library).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, touch drag (39 P3 + P5)

- Settings ▸ Scene ▸ Performance ▸ Placement preview (PlacementSettings.svelte, its own file,
  keywords + settingsIndex rows): Full model / Within budget (default) / Box only, the triangle
  budget (50k desktop, 15k phone/headset) and Show dimensions. LOCAL, safeStorage.
- Explorer ▸ Packs: a Downloaded badge (bottom right, size in its title) on every pack item
  this device holds, a "Downloaded" axis in the filter menu inside the Packs, the item menu's
  Delete -> "Delete cache (size)" (disabled when nothing is held; the item stays in the pack),
  and the pack row's "Delete downloads (size)".
- Storage: a "Pack downloads" category (one row per pack, CacheStorage — reclaimed through
  packCache) + "Clear all pack downloads"; the measured-sizes key is claimed there too.
- placing a pack item re-downloads it when its cache was deleted (a file still parsed in memory).
- touch: the place drag blocks the browser's pan with a non-passive touchmove (touch-action is
  decided at touchstart, before the long-press) — without it every finger drag ended in a
  pointercancel (measured).
- suites: place-drag 39 (+ release over chrome places nothing — proven red with App's
  bridge guard removed, 0 -> 1 —, a library model's import-time size, and the HTML5 bridge
  still moving a card into a folder), place-drag-cache 33 (downloads, filter, Delete cache,
  Storage, the preview tiers, the Settings rows, a long-press drag at 390 x 844).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…me (39 P2/P3)

- builds a 200340-tri sphere in the page, serves it as a pack item, warms it into the
  pack cache + memory, then samples rAF intervals 4 s idle vs 4 s mid-drag (pointer moving
  every 16 ms): 16.67 vs 16.67 ms, no long tasks; the ghost is the box over the 50k budget.
- run under e2e-slot --exclusive; writes after-39/39-place/perf-drag.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s (39 P1/P4)

- the chip under a finger says 'Lift to place · back to the Explorer to cancel' instead of
  the keyboard hints.
- PACKS.md: size [w,h,d] / box / tris / bytes / animated on item rows, what writes them
  (kit-build dims) and the legacy numeric size = bytes reading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uites (39 P6)

- Alt while dropping: the orbit focus point is usually in the air, so it is projected down
  onto the first surface below it (explorerDrop.surfaceBelow; ground plane fallback).
- suite place-drag-extras: a multi-selection drags as one gesture with a ghost per item in a
  row and drops as ONE undo step (one Ctrl+Z removes both); Alt = focus point on the ground;
  aiming above the horizon is red ('No place to put it here') and the release places nothing.
- vitest placementDims (13): row dims incl. the legacy numeric size = bytes, the ghost tier
  rule, an item's download urls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit c4eaf25 into feat/39 Oct 8, 2026
7 checks passed
@AlexZ005
AlexZ005 deleted the feat/39-place branch October 8, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant