Skip to content

Fly the camera to a new framing instead of cutting to it - #13

Merged
thebuilder merged 4 commits into
mainfrom
credit-the-library-and-rename-to-ldbuilder
Aug 28, 2026
Merged

thebuilder merged 4 commits into
mainfrom
credit-the-library-and-rename-to-ldbuilder

Conversation

@thebuilder

Copy link
Copy Markdown
Owner

Opening a bag moves the work to a different part of the model, and the camera followed by teleporting: one frame here, the next frame there. A cut between two views of the same model does not read as the camera having moved, it reads as the model having jumped.

Viewport.frameBox now hands the move to a flight that plays out over frames.

  • It swings rather than slides. The camera is carried as a spherical offset from the point it is looking at, so it arcs around the model the way someone walking to the other side of a table would, instead of taking the straight line through the middle of it.
  • The duration comes from the move. Target displacement, radius change and arc, scaled against how far away the camera is: 0.45s for a shift to the neighbouring subassembly, up to 1s for being carried the length of the model. Even the long end stays under a step of playback, so the build is never waiting on the camera. Easing is the easeInOutCubic that was already there.
  • The limits travel with it. Near and far planes plus the orbit min/max distance interpolate along the way. Handing OrbitControls the destination's minDistance up front would shove the camera back out of a close framing it has not arrived at yet.
  • The viewer always wins. A drag, a wheel notch or a nav key calls the rest of the flight off on the spot, so the two can never pull at once.
  • Reduced motion still snaps, and so does the first framing of a model, where there is nothing to fly from and the camera is only being placed. setModel and free build's initial frame pass instant.

Viewport.updateNavigation(dt) became updateCamera(dt), since moving the camera for a frame is now either advancing a flight or reading the keys. prefersReducedMotion moved from a private function in SceneController to lib/dom, shared with the viewport.

Verifying

Checked in a real browser with agent-browser, since the in-app preview pane freezes rAF. Burst screenshots across a FRAME toggle on the gatehouse and across a bag change on the Saturn V both show a continuous dolly through four to six distinct intermediate positions, settling on the destination rather than arriving in one frame.

pnpm test (595 passing), ultracite check and tsc --noEmit are all clean. Three existing framing tests now run the move out before asking where the camera ended up, via a framedDistance() helper that also removes a repeated cast. New tests cover the flight itself, interrupting it, and reduced motion.

For the reviewer

This branch also carries three earlier commits that predate the camera work: crediting the parts library, the rename to LDBuilder, and the licence note. The camera change is the last commit, Fly the camera to a new framing instead of cutting to it.

The attribution was wrong in four ways, all of them found by reading
CAreadme.txt in the library rather than the summary page.

Name both licence versions. Every part that ships carries CC BY 4.0 or
"CC BY 2.0 and CC BY 4.0"; only the OMR models are CCAL 2.0, which the
same document defines as a deprecated spelling of CC BY 2.0. Crediting
only 2.0 named the wrong licence for most of the geometry on screen.

Say the models were modified. CAreadme draws the derivative-work line in
exactly the place packing sits on the far side of: a model that
references library parts is not a derivative, one that includes their
source "in any form" is. Every packed root now carries a notice saying
what packing did and what it left alone. It goes under the header rather
than above it, because LDrawLoader only treats "0 FILE" as the main
model when it is on line 0, and a header is read positionally.

Credit the model's author in the viewer, not just the gallery. A deep
link to /build/galaxy-explorer showed no sign of who built it. Credit
now rides on ModelData, so it travels with the model instead of being
glued into the title string.

Stop signing other people's work. A free build is the person's own
model, so the exporter no longer stamps it "0 Author: LDraw Builder" or
licenses it on their behalf. It owes the library nothing either: the
file references parts and embeds none, which CAreadme names as the case
that is not a derivative work.

Two repairs the above needed. pack-palette passed `index` where
packModel wants `resolve` and so could not run at all, which blocked
repacking the palette; it inlines 642 part files and needs the notice as
much as a model does. And disposeModel had no tests, which is worth
fixing for its own sake: nothing observable goes wrong when teardown is
incomplete, it just leaks a few hundred megabytes per model opened.
"Quality, Brand, and the LDraw Name" says LDraw "should not appear in a
program's title and/or URL to protect the uniqueness of the LDraw
trademark", and that LDraw.org cannot grant an exception because it
licenses the mark from the Jessiman Estate rather than owning it. It
suggests an LD prefix instead, which is where LDView and LDCad get their
names.

"LDraw Builder" at github.com/thebuilder/ldraw-builder was outside that
on both counts. The README records the reasoning next to the trademark
notice, so this does not get quietly reverted as a typo later.

The packed-file notice deliberately carries no tool name, so it did not
have to change here.
There was no LICENSE at all, which on a public repo means all rights
reserved: nobody could legally fork it. A single blanket licence would
have been wrong too, because the packed .mpd files under public/ carry
other people's CC BY work and are not ours to relicense.

So LICENSE is split. MIT for src/, scripts/, demo-models/ and the root
config; then the CC BY data called out path by path, with which version
applies and why it varies at 2023-03-05, the two OMR sets named with
their authors, and a statement that the packed files are modified and
how. It also says plainly that the MIT grant covers copyright in the
code and licenses neither trademark.

NOTICES.md covers the dependencies, generated by `pnpm notices` rather
than written by hand. A notices file's only real failure mode is going
quietly stale when a dependency changes, and this repo already generates
and commits colors.generated.ts, omr-index.json and the packed models
for that reason. Now a diff in NOTICES.md is the honest answer to
whether the obligations moved.

Three things a hand-written file would have missed:

The inventory was machine-specific. Only this Mac's prebuilt binaries
install here, so a notice generated on a Mac omits what a Linux
deployment ships. The families are read out of the lockfile instead: 34
variants across three, of which 3 install locally. Siblings match on the
stripped prefix rather than startsWith, or @img/sharp-* swallows
@img/sharp-libvips-* and reports Apache-2.0 for ten LGPL packages.

libvips is LGPL-3.0-or-later, the only copyleft in the tree, so it gets
its own section instead of one row among thirty. It arrives under sharp,
which serves next/image; the claim that nothing imports next/image is
checked with git grep at generation time rather than asserted, so it
cannot rot silently.

caniuse-lite is CC-BY-4.0 and needs attribution like the parts library
does.

The rules live in scripts/lib/notices.mjs, pure and tested, with the CLI
as the I/O half. That is how ldraw-pack and omr-index are already split,
and it is what lets the audit see the coverage.
Opening a bag moves the work to a different part of the model, and the
camera followed by teleporting: one frame here, the next frame there.
A cut between two views of the same model does not read as the camera
having moved, it reads as the model having jumped, which is exactly the
thing a build video never does.

So frameBox now hands the move to a flight that plays out over frames.
The camera is carried as a spherical offset from the point it is looking
at rather than as a position, so it swings around the model the way
someone walking to the other side of a table would, instead of taking
the straight line through the middle of it. The near and far planes and
the orbit distance limits travel with it: handing OrbitControls the
destination's minimum distance up front would shove the camera back out
of a close framing it has not arrived at yet.

Duration comes from how far the move actually goes, so shifting to the
neighbouring subassembly does not take as long as being carried the
length of the model. Even the long end stays under a step of playback,
so the build is never waiting on the camera.

The viewer always wins: a drag, a wheel notch or a nav key calls the
rest of the flight off on the spot, so the two can never pull at once.
Reduced motion still snaps, and so does the first framing of a model,
where there is nothing to fly from and the camera is only being placed.
@vercel

vercel Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ldbuilder Ready Ready Preview Aug 28, 2026 12:35pm

@thebuilder
thebuilder marked this pull request as ready for review August 28, 2026 12:37
@thebuilder
thebuilder merged commit b2f6458 into main Aug 28, 2026
3 checks passed
@thebuilder
thebuilder deleted the credit-the-library-and-rename-to-ldbuilder branch August 28, 2026 12:38

This branch was successfully deployed

1 active deployment
Preview — c7464277 Deployed Aug 28, 2026 by vercel[bot]
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