Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/svelte-migration/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ same project-lock `_mutation_guard` rather than as a background job.
| `POST /api/v1/projects/{id}/clipboard/paste` | `{}` | `200 ProjectView & {changed: true}` (sync); `paste_clipboard` - requires both a populated clipboard and a selected slice (`409 not_ready` otherwise); the clipboard survives the paste. |
| `POST /api/v1/projects/{id}/slices/balance` | `{}` | `200 ProjectView & {changed}` (sync); `balance_slices` - `changed: false` (not an error) with zero slices, matching `WorkflowUnchanged`'s pattern. Implements the *fixed*, intended behavior; the equivalent Dash button is currently unreachable (500s) due to an unrelated, pre-existing `webui.py` bug - see PARITY.md "Known quirks". |
| `PUT /api/v1/projects/{id}/slices/{index}/depth` | `{depth}` | `200 ProjectView & {changed: true}` (sync); `set_slice_depth` - reordering clears the selection even when a *different* slice was selected, and always clears the inpainting candidate selection too (both are real Dash side effects; see "Known quirks"). Out-of-range `index` is `400 invalid_request`. |
| `PUT /api/v1/projects/{id}/slices/{index}/ground` | `{isGround}` | `200 ProjectView & {changed}` (sync); marks the slice as the scene's horizontal ground plane (at most one: marking one unmarks any other; `SliceView.isGround`). The ground is the plane at camera height that the bottom image row meets at `camera.groundNear`, out to `maxDistance`; ground rows beyond that (up to the horizon) go on an upright backdrop at `maxDistance`. Rendering (`scene.render_scene`) draws the ground first, then the cards far to near, so the reference camera reproduces the image. Requires the horizon above the bottom edge (`409 not_ready` otherwise); settings that would hide the ground while one is marked are `400 invalid_request`. Persisted as `ground_plane_slice` in the project JSON. |
| `PUT /api/v1/projects/{id}/slices/{index}/image` | multipart `image` | `200 ProjectView & {changed: true}` (sync); `replace_slice_image` - a mismatched-aspect-ratio upload is resized exactly like Dash's real (buggy) `slice_upload`, which can collapse the slice's dimensions and, in some slice-shape combinations, make the subsequent input-image recompose crash (`500`) - see "Known quirks". Missing/invalid multipart `image` is `400 invalid_request`. |
| `POST /api/v1/projects/{id}/mask/invert` | `{}` | `200 ProjectView & {changed: true}` (sync); `invert_mask` - creates an all-zero mask first if none exists, then inverts it. No save (matches Dash). |
| `POST /api/v1/projects/{id}/mask/feather` | `{}` | `200 ProjectView & {changed: true}` (sync); `feather_mask` - requires an existing mask (`409 not_ready` otherwise); fixed 10px `cv2.blur` kernel, matching `blur_mask`. No save. |
Expand Down Expand Up @@ -289,7 +290,7 @@ the original whenever "Upscale Textures" has produced one for that slice.
| --- | --- | --- |
| `POST /api/v1/projects/{id}/save` | – | `200 ProjectView` (sync); `save_project` - a full `AppState.to_file` (image, depth map, every slice, plus the project JSON), matching `save_state`'s own defaults exactly. |
| `GET /api/v1/projects/{id}/state-file` | – | The exact JSON payload `POST /projects/restore` accepts (`Content-Disposition: attachment; filename="appstate.json"`) - Dash has no equivalent browser download of this file (`save_state` only writes it to disk); served from the project's *current in-memory* state (`state.to_json()`), not the on-disk file, so it reflects unsaved mutations too. |
| `PUT /api/v1/projects/{id}/settings` | `{depthModel?, camera?: {distance, focalLength, maxDistance, pitch?}, meshDisplacement?, darkMode?}` | `200 ProjectView & {changed}` (sync); `update_settings` - unifies `remember_depth_model`/the persist `remember_camera_parameters`/`toggle_dark_mode` into one command; each field is only applied (and the project only re-saved, JSON-only) when both provided and different from the project's current value, exactly matching every one of those callbacks' own unchanged-value guard individually. | `camera.pitch` (degrees, ±60, positive looks up; optional, omitted = unchanged) tilts the reference camera about its x axis: cards stay vertical planes, built by back-projecting the image corners, so the reference view still reproduces the image; `Camera.pitch_for_horizon` converts a horizon row to a pitch.
| `PUT /api/v1/projects/{id}/settings` | `{depthModel?, camera?: {distance, focalLength, maxDistance, pitch?, groundNear?}, meshDisplacement?, darkMode?}` | `200 ProjectView & {changed}` (sync); `update_settings` - unifies `remember_depth_model`/the persist `remember_camera_parameters`/`toggle_dark_mode` into one command; each field is only applied (and the project only re-saved, JSON-only) when both provided and different from the project's current value, exactly matching every one of those callbacks' own unchanged-value guard individually. | `camera.pitch` (degrees, ±60, positive looks up; optional, omitted = unchanged) tilts the reference camera about its x axis: cards stay vertical planes, built by back-projecting the image corners, so the reference view still reproduces the image; `Camera.pitch_for_horizon` converts a horizon row to a pitch.
| `POST /api/v1/projects/{id}/export/gltf` | `{dof: bool}` | `202 {job}` (kind `export-gltf`); `export_gltf`, using `displacement_scale = state.mesh_displacement` (whatever `PUT .../settings` last persisted - mirrors the live slider value Dash's own callbacks read at click time). |
| `GET /api/v1/projects/{id}/export/gltf` | – | The most recent export's `.gltf` file (`Content-Disposition: attachment; filename="scene.gltf"`, inline base64 PNG data URIs, exactly like Dash's `dcc.send_file` download); `404 not_found` before any export has run. `ProjectView.exports.gltf` is this same URL (content-versioned by the file's mtime/size), or `null` when no export exists yet. |
| `POST /api/v1/projects/{id}/export/upscale` | `{}` | `202 {job}` (kind `upscale`); `upscale_textures` - builds/replaces the shared inpainting pipeline via the existing `create_inpainting_pipeline` helper using the project's current inpainting settings (mirrors `upscale_texture`'s own pipeline-replacement/upscaler-invalidation contract - see the handoff's "Cache reuse" bullet), then calls `AppState.upscale_slices()`. `ProjectView.exports.upscaled` reflects whether any slice has a `*_upscaled.png` file on disk. |
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/lib/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export type Kind = string;
export type Direction = "up" | "down" | "left" | "right" | "in" | "out" | "reset";
export type Distance = number;
export type Focallength = number;
export type Groundnear = number | null;
export type Maxdistance = number;
export type Pitch = number | null;
export type Distance1 = number;
export type Focallength1 = number;
export type Groundnear1 = number;
export type Horizonrow = number | null;
export type Maxdistance1 = number;
export type Pitch1 = number;
export type Model = string;
Expand Down Expand Up @@ -78,6 +81,7 @@ export type Canredo = boolean;
export type Canundo = boolean;
export type Depth = number;
export type Index = number;
export type Isground = boolean;
export type Negativeprompt2 = string;
export type Positiveprompt2 = string;
export type Version1 = number;
Expand Down Expand Up @@ -105,6 +109,7 @@ export type X1 = number;
export type Y1 = number;
export type Slice = number | null;
export type Usecheckerboard1 = boolean;
export type Isground1 = boolean;
export type Depth1 = number;
export type Numslices1 = number;
export type Baserevision = number;
Expand Down Expand Up @@ -167,6 +172,7 @@ export interface CameraNavigateRequest {
export interface CameraSettingsRequest {
distance: Distance;
focalLength: Focallength;
groundNear?: Groundnear;
maxDistance: Maxdistance;
pitch?: Pitch;
}
Expand All @@ -177,6 +183,8 @@ export interface CameraSettingsRequest {
export interface CameraSettingsView {
distance: Distance1;
focalLength: Focallength1;
groundNear?: Groundnear1;
horizonRow?: Horizonrow;
maxDistance: Maxdistance1;
pitch?: Pitch1;
}
Expand Down Expand Up @@ -446,6 +454,7 @@ export interface SliceView {
depth: Depth;
image: AssetRef;
index: Index;
isGround?: Isground;
mask?: AssetRef | null;
negativePrompt: Negativeprompt2;
positivePrompt: Positiveprompt2;
Expand Down Expand Up @@ -548,6 +557,15 @@ export interface SelectionRequest {
export interface SetCheckerboardRequest {
useCheckerboard: Usecheckerboard1;
}
/**
* Body of ``PUT /projects/{id}/slices/{index}/ground``.
*
* This interface was referenced by `ParallaxMakerApi`'s JSON-Schema
* via the `definition` "SetGroundPlaneRequest".
*/
export interface SetGroundPlaneRequest {
isGround: Isground1;
}
/**
* This interface was referenced by `ParallaxMakerApi`'s JSON-Schema
* via the `definition` "SetSliceDepthRequest".
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/lib/api/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@
"title": "Focallength",
"type": "number"
},
"groundNear": {
"anyOf": [
{
"minimum": 0.0,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"title": "Groundnear"
},
"maxDistance": {
"minimum": 0.0,
"title": "Maxdistance",
Expand Down Expand Up @@ -127,6 +140,23 @@
"title": "Focallength",
"type": "number"
},
"groundNear": {
"default": 0.0,
"title": "Groundnear",
"type": "number"
},
"horizonRow": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"title": "Horizonrow"
},
"maxDistance": {
"title": "Maxdistance",
"type": "number"
Expand Down Expand Up @@ -1118,6 +1148,21 @@
"title": "SetCheckerboardRequest",
"type": "object"
},
"SetGroundPlaneRequest": {
"additionalProperties": false,
"description": "Body of ``PUT /projects/{id}/slices/{index}/ground``.",
"properties": {
"isGround": {
"title": "Isground",
"type": "boolean"
}
},
"required": [
"isGround"
],
"title": "SetGroundPlaneRequest",
"type": "object"
},
"SetSliceDepthRequest": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -1168,6 +1213,11 @@
"title": "Index",
"type": "integer"
},
"isGround": {
"default": false,
"title": "Isground",
"type": "boolean"
},
"mask": {
"anyOf": [
{
Expand Down
5 changes: 5 additions & 0 deletions parallax_maker/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ def update_settings(project_id: str):
else None
),
pitch=payload.camera.pitch if payload.camera is not None else None,
ground_near=(
payload.camera.ground_near
if payload.camera is not None
else None
),
mesh_displacement=payload.mesh_displacement,
dark_mode=payload.dark_mode,
)
Expand Down
7 changes: 7 additions & 0 deletions parallax_maker/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def _build_project_view(
can_redo=image_slice.can_undo(forward=True),
positive_prompt=image_slice.positive_prompt,
negative_prompt=image_slice.negative_prompt,
is_ground=image_slice.is_ground_plane,
image=_asset_ref(
project_id, f"slice-{index}", file_version(Path(image_slice.filename))
),
Expand Down Expand Up @@ -232,6 +233,12 @@ def _build_project_view(
focal_length=state.camera.focal_length,
max_distance=state.camera.max_distance,
pitch=state.camera.pitch,
ground_near=state.camera.ground_near,
horizon_row=(
state.camera.horizon_row(*state.imgData.size)
if state.imgData is not None
else None
),
),
mesh_displacement=state.mesh_displacement,
depth_model=state.depth_model_name or "",
Expand Down
14 changes: 14 additions & 0 deletions parallax_maker/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class SliceView(ApiModel):
image: AssetRef
thumbnail: AssetRef
mask: AssetRef | None = None
#: This slice is the horizontal ground plane rather than a vertical card.
is_ground: bool = False


class BusyView(ApiModel):
Expand Down Expand Up @@ -114,6 +116,10 @@ class CameraSettingsView(ApiModel):
max_distance: float
#: Upward tilt in degrees (negative looks down); see ``Camera.pitch``.
pitch: float = 0.0
#: Card depth where the ground plane meets the image's bottom edge.
ground_near: float = 0.0
#: Image row of the horizon for the current pitch (null without an image).
horizon_row: float | None = None


class ProjectSettingsView(ApiModel):
Expand Down Expand Up @@ -234,6 +240,12 @@ class SetSliceDepthRequest(ApiModel):
depth: float


class SetGroundPlaneRequest(ApiModel):
"""Body of ``PUT /projects/{id}/slices/{index}/ground``."""

is_ground: bool


class SetCheckerboardRequest(ApiModel):
use_checkerboard: bool

Expand Down Expand Up @@ -303,6 +315,7 @@ class CameraSettingsRequest(ApiModel):
focal_length: float = Field(gt=0.0)
max_distance: float = Field(ge=0.0)
#: Optional (older clients omit it): upward tilt in degrees.
ground_near: float | None = Field(default=None, ge=0.0)
pitch: float | None = Field(
default=None, ge=-MAX_PITCH_DEGREES, le=MAX_PITCH_DEGREES
)
Expand Down Expand Up @@ -411,6 +424,7 @@ def public_models() -> list[type[BaseModel]]:
InpaintingSelectionRequest,
InpaintingApplyRequest,
CameraNavigateRequest,
SetGroundPlaneRequest,
ProjectSettingsRequest,
GltfExportRequest,
AnimationExportRequest,
Expand Down
25 changes: 25 additions & 0 deletions parallax_maker/api/slice_editing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
RemoveMaskFromSlice,
ReplaceSliceImage,
SetCheckerboard,
SetGroundPlane,
SetSliceDepth,
SliceEditingUnchanged,
refresh_selection_preview,
Expand Down Expand Up @@ -227,6 +228,30 @@ def set_slice_depth(project_id: str, index: int):
view = _build_project_view(runtime, project_id, state)
return _mutation_response(view, True)

@blueprint.put("/projects/<project_id>/slices/<int:index>/ground")
def set_ground_plane(project_id: str, index: int):
state = _load_state(project_id)
payload = _parse_json_body(request, schemas.SetGroundPlaneRequest)
record = runtime.projects.ensure(project_id)
_validate_slice_index(state, index)

with _mutation_guard(record):
result = runtime.slice_editing_service.set_ground_plane(
SetGroundPlane(
state_id=project_id, slice_index=index, is_ground=payload.is_ground
)
)
if result.changed:
record.log.append(
f"Slice {result.slice_index} is now the ground plane"
if result.is_ground
else f"Slice {result.slice_index} is no longer the ground plane"
)
record.bump_revision()

view = _build_project_view(runtime, project_id, state)
return _mutation_response(view, result.changed)

@blueprint.put("/projects/<project_id>/slices/<int:index>/image")
def replace_slice_image(project_id: str, index: int):
state = _load_state(project_id)
Expand Down
Loading
Loading