Commit Graph
77 Commits
Author SHA1 Message Date
David Mann fff01d8918 Integration: stack #7335 (its merge onto main, conflicts resolved) 2026-10-22 23:40:11 +02:00
David MannandClaude Opus 5.5 6c7e920d5a Merge PR #7335: Gradient Overlay: add Method selector and retain editable source stops
- crates/ui-egui/src/layer_style.rs: values_of keeps the PR's method/smoothness keys and source-stop ends, and main's (#7072) unconditional From/To and geometry fields.
- crates/compose/src/cmyk_tests.rs (no textual conflict): add the PR's new `interpolation: None` field to main's CMYK test GradientOverlay literal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:39:14 +02:00
David MannandClaude Opus 5.5 cab3ef6191 Merge PR #5815: perf(doc): share the XMP packet, Duotone PSD data and unsupported adjustment bytes between snapshots
- crates/engine/src/tests.rs: kept both tests (main's restore_layer_selection test and the PR's history_states_share_large_document_payloads).
- crates/io/src/psd_import.rs: kept main's parsed Duotone inks (duotone_map) with the PR's Arc<[u8]> psd_raw; also main's unknown-adjustment-key import (#6575) now builds Arc<[u8]> raw.
- crates/io/tests/adjust_warnings.rs: moved to tests/it/adjust_warnings.rs on main; PR's .into() edits applied there, plus #6322's Color Lookup test (raw.as_slice().into()).
- docs/architecture.md: union; status line names both changes, both revision-history rows kept.
- Adapted main-new sites to the PR's types: psd_export.rs (Duotone write-back via as_deref/to_vec, Unsupported block compare by slice), inspect.rs and ui-egui unsupported_marker_tests.rs (raw .into()), io tests/it/psd_unknown_adjustment.rs (slice compare).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:38:56 +02:00
David Mann 9487065314 Integration: merge main (one test binary per crate, #8644)
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/scorecard.md
#	scorecard/file_compat.toml
2026-10-22 19:55:06 +02:00
David MannandClaude Opus 5.5 f5533e5d82 Integration: merge main into batch 6
Main merged #5940, #6472, #7838 and #7922 directly. Kept from the batch:
the snapshot/History-follow identity at the history limit (#5940,
c7ef3676a; main still had the 3-tuple HistorySpot) and the strokeless
shape at a 0 px width step (#6472, 962539607; main still added the
stroke). Main already carries the other two fix-ups (34c9d0ea4, dff8bfd0e).

Overlaps with main resolved:
- #5842 vs main's #5568: one path, apply_color, tool-gated as #5842
  has it, with #5568's float fill, kept fill opacity and lock skip;
  shape_fill_ui's duplicate foreground_changed/gesture dropped; #5568's
  tests run with a shape tool.
- #6230 vs main's #5180: one Type tool path click (path_target), no
  second type.attachToPath; vertical type does not start type on a path,
  as main has it; main's path_at/path_cursor and canvas arm dropped.
- #5452 moved the shape swatch to shape_paint_ui.rs: main's hex field
  (#6201) ported there.
- type_extra_cmds::with_text takes main's dpi form, pub(crate) for
  type_area_cmds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:20:26 +02:00
aiongg a6428d3dfb Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.

These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
2026-10-22 16:55:34 +00:00
David MannandClaude Opus 5.5 7a54fa9c77 Integration: merge main into batch 6
Conflicts, both sides kept: main's .act swatches next to the batch's .asl
styles (Open filters, preset extensions), main's Render smart filters next
to the batch's Distort ones (53 mapped), parity doc status lines.
docs/scorecard.md regenerated.

Follow-ups the merge needed: the Distort smart filter readers pass their
enum type to main's stricter enum_name (the type their writers use), and
the CMYK effect tests set main's new StrokeFx overprint and Bevel
gloss_anti_alias.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:09:25 +02:00
Yonghyuk Choi dc029b93c5 Merge main into Gradient Overlay method branch 2026-10-22 12:38:20 +09:00
aiongg 2c2269ff0b Merge PR #5421: fix(format): serialise directory bundle saves with a bundle-wide lock
crates/format/src/store.rs: kept both field sets in PcraftWriter::default (main's blob_hash_cache/compressed_budget and the PR's lock_wait) and both setters (set_compressed_cache_budget, set_lock_wait).
2026-10-22 02:24:47 +00:00
aiongg d71ff0d34d Merge PR #7580: Preserve saved PCraft previews when manifest replacement fails 2026-10-21 21:55:49 +00:00
David Mann a8f91f54ad Merge PR #6230: feat(type): area type, text that wraps inside a closed path or shape
Integration: resolve with main for area type (#6230)

- Image Size with a new resolution (#7124, canvas_geom::set_resolution) now scales an area type's
  path with the text space like a box or a type path, so the same words wrap onto the same lines;
  test a_resolution_change_keeps_area_type_wrapping_where_it_was fails without it.
- The Paragraph panel's justify buttons (#7713) stay available on area type, which is paragraph
  text in Photoshop; test justify_buttons_work_on_area_type.
- Test files: main's tests plus #6230's; layout.rs imports joined; docs and scorecard regenerated.
2026-10-21 23:46:05 +02:00
aiongg 94d28535d3 Merge PR #6464: Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215) 2026-10-21 18:15:02 +00:00
aiongg 77557d75f1 Merge PR #8229: Integration batch: 8 PRs on main
# Conflicts:
#	crates/engine/src/layer_style.rs
#	crates/io/src/psd_export.rs
#	crates/io/src/smart_map.rs
#	docs/file-format-parity.md
2026-10-21 15:45:07 +00:00
David Mann a492716367 Merge PR #5990: Bevel & Emboss: keep the Gloss Contour's Anti-aliased (antialiasGloss) 2026-10-21 11:29:05 +02:00
aiongg fc371711e6 Merge PR #5847: Shapes: Density and Feather on shape layers (#5846)
# Conflicts:
#	docs/scorecard.md
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-21 05:04:03 +00:00
0073b395d4 Integration: merge main, resolve conflicts (#5990)
docs/file-format-parity.md: main added three 2026-10-15 revision rows
(DDS save, PNG sTER, PSD guides) where the PR added its own; keep all four.

Co-authored-by: varanus-salvator <262257003+varanus-salvator@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:50:13 +02:00
David Mann 2adfe0c4e2 Merge PR #5807: Type: keep superscript and subscript when a text layer is re-rendered (#5645) 2026-10-20 22:39:12 +02:00
David Mann 0b39361076 Merge main into feat/tool-214-12-shape-feather 2026-10-20 17:12:29 +02:00
00xJ0 abd3755d41 RecoveryStore::flush keeps each entry's key, so a later discard still removes it (#6464 review) 2026-10-19 12:05:01 -07:00
PanPDX 54b6b0dbc0 Merge remote-tracking branch 'upstream/main' into area-type
# Conflicts:
#	crates/ui-egui/src/type_tool.rs
#	docs/gaps.md
#	docs/scorecard.md
#	docs/target-app-parity.md
2026-10-18 22:41:59 -07:00
00xJ0 7bb7819b00 Lost window device: close only once every unsaved document is saved for recovery, retrying every 2 s; also check the device on the screen-picker frame (#6464 review) 2026-10-18 17:15:13 -07:00
jonathnwxh 86b4c67adf Expect post-commit best-effort preview cleanup on Unix 2026-10-19 05:37:19 +08:00
jonathnwxh 9c927993b5 Preserve omitted PCraft previews until manifest commit 2026-10-19 05:29:30 +08:00
Finn NewickandClaude Opus 5.5 e795cabc79 test(format): skip the bundle-lock test only where file locks are unsupported
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:16:37 +01:00
NetterSilverandClaude Haiku 5.5 0807441e52 Make .pcraft reload f64 coordinates bit-exact (#7498)
Found while testing crates/format/Cargo.toml:19 (serde_json features), used for the manifest in crates/format/src/manifest.rs and store.rs

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 16:56:48 -04:00
Finn Newick 1e8dcb53b9 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock 2026-10-18 21:21:28 +01:00
Finn Newick d8053570c7 Merge remote-tracking branch 'origin/main' into perf/doc-shared-blobs 2026-10-18 21:21:26 +01:00
00xJ0 4615a8bc26 Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215)
When the window's own wgpu device was lost (a driver reset, e.g. after sleep), egui-wgpu could
not draw another frame: a frame with shapes panicked inside it ("Failed to create staging buffer
for index data", #1215, exit 101, unsaved work lost), and a frozen window's close button opened
an unsaved-changes prompt nobody could see (#3438). The #243 fallback only covered the canvas.

- DeviceHealth::device_lost (wgpu reported the loss, not a recorded fault) and ::probe (a
  non-blocking poll that keeps running after a recorded fault).
- RecoveryStore::flush: finish queued autosaves and wait for them.
- gpu_status::check_window, at the end of every frame: once the window's device is lost the
  frame draws nothing; the first time every unsaved document is written to crash recovery
  (Services::flush_autosaves) and the app closes without asking. The next launch reopens them.
- The app watches the window device on the CPU canvas too; ui.gpu.simulateLoss {destroy: true}
  destroys the real device to drive the path.

Verified on an RX 6900 XT, Vulkan and DX12: destroying the device in the running app exited 101
with the egui-wgpu panic before, and now exits 0 with the document recovered on the next launch.
2026-10-18 12:53:48 -07:00
PanPDX b2f18f7e4e Merge remote-tracking branch 'upstream/main' into area-type 2026-10-18 12:47:22 -07:00
oRE-o 8fb5d94d2b Gradient Overlay: expose interpolation methods and retain source stops 2026-10-18 18:36:30 +09:00
David MannandClaude Opus 5.5 921637322c Native format: refuse a .pcraft with out-of-range numbers when it is opened (#6862)
A number past f32's range (e.g. opacity 1e39) decoded as infinity, so the document opened but
could never be saved again; read_manifest now refuses it as corrupt, naming the field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:30:16 +02:00
vangel76andClaude Opus 5.5 489feb2333 Tests: pcraft keeps text run baseline; tighten sub/superscript size check (#5645)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:57:15 +02:00
Finn Newick a1d1e48548 Merge remote-tracking branch 'origin/main' into perf/doc-shared-blobs 2026-10-15 10:37:01 +01:00
PanPDXandClaude Opus 5.5 d2856e6ecb test(format): area type round-trips through .pcraft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:51:37 -07:00
varanus-salvatorandClaude Opus 5.5 92aa5047cd Bevel & Emboss: keep the Gloss Contour's Anti-aliased (antialiasGloss)
Photoshop stores Bevel & Emboss › Gloss Contour › Anti-aliased as
`antialiasGloss` in the effect descriptor. The document model had no
field for it, so import dropped it and export regenerated the bevel
without it: once a bevel was edited (the Layer Style dialog re-applies
all its settings), a saved PSD reopened in Photoshop with the gloss
contour aliased.

Add `Bevel::gloss_anti_alias` (serde default, so older .pcraft files
load), read and write `antialiasGloss`, accept `glossAntiAlias` in
layer.layerStyle.bevelEmboss and show the Anti-aliased checkbox next to
the Gloss Contour in the dialog, as Photoshop does. Rendering is
unchanged: contour anti-aliasing is not rendered for any effect yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:31:59 +07:00
David MannandClaude Opus 5.5 6bb45c8d5b Shapes: Density and Feather on shape layers (#5846)
A shape layer's path is its vector mask in Photoshop (Properties > Masks),
with its own vector-mask density and feather. PhotoCraft's ShapeLayer had
neither, so soft shapes could not be made, and stroked soft shapes from PSDs
lost both on import.

- doc: ShapeLayer gains `density` (0..1, default 1) and `feather` (px,
  default 0).
- vector: the shape renderer applies them to the fill as a soft vector mask
  (fill shows beyond the path at 1 - density; the feather is the vector
  mask's Gaussian), so the shape's own pixels, the GPU (which samples them)
  and the stroke/fill split all follow. The stroke stays crisp. The feather
  blur moves from compose::masks to vector::feather and both share it and the
  feathered area, so a shape and an equal vector mask render the same pixels.
- compose: soft shapes' bounds reach their fill, their layer effects follow
  the soft alpha, and the split cache key includes density and feather.
- engine: `layer.vectorMask.edit` on a shape layer sets density/feather in one
  history step (path/enabled/linked/invert are refused there); density is now
  validated (finite 0..100) instead of clamped. `layer.vectorMask.info`
  reports a shape's values.
- ui: Properties shows the Vector Mask Density and Feather sliders for a
  selected shape layer in both themes (live drag, one undo per gesture).
- .pcraft: manifest and convert carry both with serde defaults (older files
  load at 100 % and 0 px).
- PSD: export writes them as the vector density/feather mask parameters;
  import reads them into the shape (re-rendered, as the stored pixels lack the
  fill beyond the path). Soft unstroked solid/gradient shapes now import as
  shapes instead of fill layers with a vector mask; pattern-filled ones stay
  fill layers.

Photoshop's 10 px feather on a 100 px black square, row 100, x = 30..72:
ours is within 1 level of every measured value (8, 16 and 32 bits);
density 50 % shows 127/128 beyond the path and 0 inside.

Scorecard: TOOL-214-12 done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 03:07:46 +02:00
Finn Newick d770fd35f1 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock
# Conflicts:
#	crates/format/tests/roundtrip.rs
2026-10-14 23:12:45 +01:00
mroxsoandhighperfocused 57d488032c fix(doc): variable definitions follow layer id remapping on copy and load (#5023)
* fix(doc): variable definitions follow layer id remapping on copy and load

A duplicated document (image.duplicate, history.newDocument) or a .pcraft
loaded with remapped ids assigned fresh layer ids but cloned VariableDef.layer
unchanged, so datasets silently drove no layer (or, after remap, a colliding
one). Remap variable targets through the same id map as layer comps and
slices; a dangling target keeps a non-colliding placeholder. A merged-only
duplicate drops the whole variable binding set, matching how it drops comps.

Closes #4969

* style: cargo fmt duplicate_variables test

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-14 15:02:43 -07:00
Finn NewickandClaude Opus 5.5 0351f635d7 perf(doc): share the XMP packet, Duotone PSD data and unsupported adjustment bytes between snapshots
Every edit clones the document and history keeps one per state. Metadata.xmp (String), Duotone.psd_raw (Vec<u8>) and Adjustment::Unsupported.raw (Vec<u8>) were owned, so each copy duplicated them, uncounted; their siblings (EXIF, PSD resources and blocks, a layer's psd_raw) were already Arc. They are now Arc<str> and Arc<[u8]>. Reads are unchanged (Arc<str> derefs to str); the boundaries to the codecs, the .pcraft manifest and the PSD model convert once per file operation. The architecture's snapshot principle now states the rule for every large payload, and a test checks that a history state shares all three. Fixes #4535.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:08:33 +01:00
Finn NewickandClaude Opus 5.5 d658c13c4e fix(format): serialise directory bundle saves with a bundle-wide lock
A directory save lists the existing objects, writes its own, publishes its manifest and removes what that manifest doesn't reference. Two writers (two stores, or two processes) saving the same bundle could interleave, and one writer's cleanup could remove an object the other's winning manifest needs. Each save now holds an OS advisory lock on save.lock for its whole transaction; a held lock is waited for (10 s by default, PcraftWriter::set_lock_wait), then the save fails without writing. A crash releases the lock; a file system without file locks saves unlocked as before. Fixes #4957.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:12:49 +01:00
David MannandClaude Opus 5.5 b41e34c20f Rulers: drag the zero point from the ruler corner, double-click resets it (#2955) (#3505)
Drag out of the rulers' upper-left corner to move the zero point: cross hairs mark where it
lands, Shift snaps it to the ruler ticks and View > Snap To targets apply otherwise; dropping it
back on the corner cancels. Double-clicking the corner puts it back at the image's top-left.
The rulers then read 0 at the zero point (negative left of and above it) and the Info panel's
X/Y measure from it.

The zero point is a document field (`ruler_origin`, document pixels), saved in .pcraft with
`#[serde(default)]` so older files open at the top-left. It is set by two undoable engine
commands, `view.setRulerOrigin {x, y}` and `view.resetRulerOrigin`, so agents can drive it.
It is not written to PSD: the public PSD spec documents no resource for it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:24:17 -04:00
shmuel billerandskylar taylor-barrick 2d5a7497fd fix(format): harden directory-bundle cleanup
Protect Unix bundle cleanup with pinned directory handles and remove only recognized content-addressed files. Keep legitimate linked roots and write/search-only directories working, with regression coverage for GC races and permission failures.

Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-13 09:20:31 -06:00
15aac8d7b6 Type: type on a path, glyphs follow the curve (#4365) (#4377)
* Type: type on a path, glyphs follow the curve (#4365)

Add TextShape::Path (path, start offset, flip). The text is laid out as one
point-text line and each glyph is placed rigidly on the flattened path by arc
length, turned to the tangent at its centre; glyphs past the end are hidden and
a degenerate path falls back to straight text. New commands type.attachToPath
(work path or shape layer subpath) and type.detachFromPath. TextShape is no
longer Copy. .pcraft round-trips the new variant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Automation policy: type.attachToPath/detachFromPath take document vector paths, not host files (#4365)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: regenerate scorecard after merging main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 23:11:09 -04:00
73a56d7603 Format: overflow-safe blob decode and bounds-checked ZIP reads (#3486) (#3501)
- store: `max_blob_bytes as u64 + 1` overflowed for usize::MAX (debug
  panic; release take(0), so every blob failed). The blob read and the
  tile decode now share decompress_bounded(), which saturates.
- zip: ZipReader::read sliced `data_offset..data_offset + compressed`
  unchecked although Entry is pub + Clone; a foreign entry is now a
  Corrupt error.
- store: Collect::tile reuses tile_le(); Object::content() gives the
  stored bytes once (existing_object_is_valid no longer builds them twice).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:08:20 -04:00
fd21e73411 Format: check pattern sizes on load (#3478) (#3484)
* Format: check pattern sizes on load (#3478)

A .pcraft pattern's width/height came straight from the manifest, so a
1 KB file could declare a 65535x65535 pattern without tiles; painting it
allocated width*height*16 bytes (abort) and the engine's u32 size math
overflowed.

- format: reject patterns 0 px on a side (Corrupt) or above the PSD
  reader's 30 000 px edge (LimitExceeded).
- doc: Pattern::pixel_count / is_paintable with MAX_PAINT_PIXELS (2^26,
  a 1 GiB f32 RGBA working copy).
- compose/gpu: patterns too large to paint paint nothing instead of
  allocating (Tile::new_in, pattern_rgba, PreparedPatterns, GPU plan).
- engine: brush.texturePattern refuses them; average_color uses u64 math
  and a bounded corner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* gpu: skip unpaintable patterns in the Bevel texture too

The Bevel & Emboss texture element uploaded the pattern through
pattern_rgba, which now returns no pixels for a pattern too large to
paint; filter it like the other GPU pattern lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:06:16 -04:00
skylar taylor-barrick 61b6276c78 Security: harden document exports and automation inputs (#2269)
* chore: clear strict Clippy simplifications

* security: harden web exports and generated image assets

* security: reject redirected directory bundle writes

* security: quote paths in droplet shell launchers

* security: use private ephemeral print spool files

* security: bound automation file reads and MCP input

* security: bound MCP transport requests before dispatch

* chore: keep current sample checks Clippy clean

* security: preserve safe export roots and print semantics

* security: clear non-sending print state before replay

* security: forget print send overrides between jobs

* chore: keep macOS UI tests and Clippy clean

* Test web export paths portably on Windows
2026-10-11 23:18:52 -06:00
FinnNkandClaude Opus 5.5 962e0a2f98 feat(ui): Photoshop's New Guide: units and colour (#3553)
* feat(engine): accept Photoshop units for guide positions

view.newGuide and view.moveGuide take the position as before (a number
of pixels) or as text with a unit, as Photoshop's New Guide dialog does:
"50%", "2.5 cm", "1in", "12pt", "3pc", "100px". Percent is of the
document's width for a vertical guide and its height for a horizontal
one; points and picas follow Units & Rulers › Point Size. Fractional
positions are kept. Text that isn't a length, NaN and positions beyond
1,000,000 px are refused without recording a history step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(engine): guides can have their own colour

Photoshop's New Guide sets a colour per guide; PhotoCraft guides only had
the Preferences guide colour. Guides now keep optional colours, keyed by
orientation and position so code that drops or reorders guide positions
(crop, artboards, guide layouts) can never move a colour onto another
guide: an unmatched guide just uses the preference colour.

view.newGuide takes an optional "color": a name from New Guide's list
(lightBlue, lightRed, green, mediumBlue, yellow, magenta, cyan, lightGray,
black) or "#rrggbb". Without one the guide uses the preference colour, as
guides dragged from the rulers always have. Moving a guide carries its
colour; deleting and clearing drop it (Clear Guides, Clear Canvas Guides,
Clear Selected Artboard Guides and New Guide Layout's Clear Existing
Guides alike); undo restores it. Guides are drawn in their own colour
(while New Guide Layout previews, the rest show the colour it will set), and .pcraft files keep it (omitted when no guide has
one, so older readers and files are unaffected). PSD export keeps guide
positions only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): Photoshop's New Guide dialog: units and colour

View › Guides › New Guide… was the generic schema dialog, whose position
field was whole pixels only and had no colour. It now has its own
dialog, as in Photoshop:

- Position opens in the ruler unit ("0 cm") and takes any unit
  Photoshop's field takes ("50%", "2.5 cm", "12 pt"); a bare number is
  in the ruler unit. The last guide reopens in whatever the ruler unit
  is then ("5.08 cm").
- Color lists Custom…, Light Blue, Light Red, Green, Medium Blue,
  Yellow, Magenta, Cyan (the default), Light Gray and Black. Custom…
  shows a swatch that opens the Color Picker. The guide stores the
  chosen colour itself, so a later change of the preference guide
  colour leaves it alone.
- A position the engine refuses reopens the dialog with what was typed
  and the engine's message, as Photoshop keeps New Guide open.

Light Blue, Light Red, Medium Blue and Light Gray are new to the
catalogues; they were translated for all 16 languages from the existing
colour terms in each and would benefit from native review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(engine): one unit-suffix table for typed lengths

New Guide's position field (Unit::parse_length) and the Crop tool's size
fields (crop_size::unit_of) each kept the same table of unit suffixes.
Unit::from_suffix is now the one table: px, in or ", cm, mm, pt, pc or
pica, % and their long forms, in any case. The crop fields keep their own
number parsing (they accept arithmetic) and still refuse percent; they now
also accept "pc" for picas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): New Guide and New Guide Layout share one colour list

New Guide Layout (#2930) offered five colours of its own; New Guide offers
Photoshop's nine. Both dialogs now list the same nine (GUIDE_COLORS), in
Photoshop's order and with the same translated names. Cyan (#4affff, the
default guide colour), Magenta (#ff00ff) and Yellow (#ffcc00) take New
Guide Layout's values; Light Blue, Light Red, Green, Medium Blue, Light
Gray and Black keep New Guide's. New Guide Layout's Blue (#378ef0) and
jade Green (#00a86b) give way to Photoshop's Light Blue and Green; a colour
outside the list still shows as Custom.

GUIDE_COLORS drops its unused label column (the dialogs translate the
names), and New Guide Layout reads "#rrggbb" with prefs::parse_hex instead
of its own copy (rulers::parse_guide_color).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:46:56 -04:00
Valentin KleinandClaude Opus 5.5 c20b81b251 Autosave: best-effort cleanup after commit; sweep orphaned recovery bundles (#3489) (#3502)
save_dir removed unreferenced objects with `remove_file(..)?` after the
manifest commit, so a vanished or locked file made a completed save
(autosave) report failure. The cleanup is now best-effort.

RecoveryStore::new sweeps recovery data nothing could load or clean up:
bundles without a sidecar (crash during a first autosave) and
*.pcsave.tmp files of other processes, only once untouched for 24 hours
so another instance's in-progress save is never touched.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:31:23 -04:00
Valentin KleinandClaude Opus 5.5 60ef9d836f Format: reopening a .pcraft shares deduplicated tiles again (#3315) (#3339)
Saves store each distinct tile once (a solid fill shares one Arc<Tile>), but
the loader fetched, decompressed and charged max_total_bytes once per tile
reference and gave every coordinate its own copy. A solid canvas above
~65536² (8-bit) failed to reopen with "bundle expands beyond 17179869184
bytes", and smaller ones lost their sharing (a solid 30000² 32-bit document
went from about 1 MB to about 14 GB in memory).

The loader now keeps the decoded tiles of the current load by sample type and
content hash: each distinct tile is decoded, hash-checked and charged against
the budget once, and every further reference gets the same copy-on-write Arc.
The budget still bounds distinct decoded bytes; extra references cost a map
lookup and an Arc clone each, bounded by max_manifest_bytes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:21:45 -04:00
Valentin KleinandClaude Opus 5.5 6e9ba6511d Format: admit the declared .pcraft canvas before loading it (#2935) (#3295)
* Format: admit the declared .pcraft canvas before loading it (#2935)

A .pcraft manifest's canvas size was copied into the Document unchecked, so
a few bytes of JSON could declare a 300000x300000 (or u32::MAX) canvas that
the CPU reduction, thumbnails and display textures then work against in
proportion to its area.

The loader now refuses an empty canvas, a side above 300000 (Photoshop's PSB
limit, as for PSD and file.new) and an area above 4 gigapixels, right after
reading the manifest and before any tile or blob is decoded. Both limits are
LoadOptions fields so trusted callers can change them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format: admit every canvas the app can create on .pcraft load (#2935)

The default pixel budget was 2^32 (65536 x 65536), but file.new, Canvas
Size, Image Size with Resample off and PSB import all reach 300000 x
300000, so a document a user could create and save would not reopen.
The default is now the full 300000^2 PSB canvas; the side limit still
refuses anything larger, and callers handling untrusted files can lower
either budget. Loading stays proportional to the file: layers keep only
the tiles the bundle holds, so a huge declared canvas allocates nothing
for its area.

A compile-time assert in document_preset_cmds ties file.new's
MAX_DIMENSION to the loader's defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:22:11 -04:00