Skip to content

Add SVG-to-3D print designer (Phase 1 MVP) - #82

Open
ayhanars wants to merge 24 commits into
kirkas:masterfrom
ayhanars:claude/svg-3d-print-designer-motvtz
Open

Add SVG-to-3D print designer (Phase 1 MVP)#82
ayhanars wants to merge 24 commits into
kirkas:masterfrom
ayhanars:claude/svg-3d-print-designer-motvtz

Conversation

@ayhanars

@ayhanars ayhanars commented Sep 2, 2026

Copy link
Copy Markdown

Standalone browser app under svg3d-designer/, built with Vite + React + TypeScript + three.js/R3F. Implements the SVG -> 3D -> STL Phase 1 workflow: a Figma-style shell (layer panel, 2D/3D canvas, inspector), SVG import that preserves group hierarchy as nested layers (holes and compound paths resolved correctly), 2D pan/zoom/select/move, per-layer color and independent extrusion depth synced live between the 2D and 3D views, a configurable print bed, and binary STL export built from the same scene graph shown on screen.

Lives alongside the existing Ascensor.js jQuery plugin without touching it.

Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

claude and others added 4 commits September 2, 2026 10:01
Standalone browser app under svg3d-designer/, built with Vite + React +
TypeScript + three.js/R3F. Implements the SVG -> 3D -> STL Phase 1
workflow: a Figma-style shell (layer panel, 2D/3D canvas, inspector),
SVG import that preserves group hierarchy as nested layers (holes and
compound paths resolved correctly), 2D pan/zoom/select/move, per-layer
color and independent extrusion depth synced live between the 2D and
3D views, a configurable print bed, and binary STL export built from
the same scene graph shown on screen.

Lives alongside the existing Ascensor.js jQuery plugin without touching
it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC
Builds the app and publishes it to GitHub Pages on every push, so it's
reachable at a live URL without anyone needing to clone and run it
locally. Sets vite's base path to /Ascensor.js/ only inside Actions
(GITHUB_ACTIONS is set automatically there) so local dev/build is
unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC
…sets (#2)

Six requested features on top of the Phase 1 MVP:

- Light/dark mode, following the OS preference until the user toggles it
  explicitly (persisted to localStorage); the 3D viewport background
  follows the theme too.
- Merge/flatten layers (Figma's Flatten Selection): combines multiple
  selected layers, or everything inside one selected group, into a single
  flat shape with each source's world transform and corner rounding baked
  in. Handles mixed selections (a group plus its own child, items from
  different parents) without corrupting the layer tree.
- Undo/redo via zundo, wired to Cmd/Ctrl+Z, Shift+Z and toolbar buttons,
  plus Cmd/Ctrl+D duplicate, Cmd/Ctrl+A select all, Cmd/Ctrl+E merge,
  Delete/Backspace, and Escape — all skipped while typing in a text field.
  Continuous gestures (canvas drag, the corner-radius slider) collapse
  into a single undo step via an explicit begin/end-gesture pause.
  Selection changes and view-only toggles (2D/3D, grid) are intentionally
  excluded from history, matching how Figma's undo behaves.
- Corner radius (smoothness): rounds every vertex of a shape's contours
  via tangent-circle fillets, independently clamped per corner so
  neighboring fillets can never overlap. Applied identically in the 2D
  preview, 3D view, and STL export — never just a cosmetic preview effect.
- Artboard sizing: fit the document to the current selection's bounds, or
  match it to the configured print bed.
- Full Bambu Lab bed presets (X1 Carbon, X1, X1E, P1S, P1P, A1, A1 mini,
  H2D) replacing the earlier placeholder list.

Verified interactively in a real browser (Playwright): import, multi-select
merge across different edge cases, corner-radius drag with undo/redo,
dark mode, all keyboard shortcuts, and a re-exported STL with a rounded
corner confirmed to have a valid binary structure, correct mm-scale
bounding box, and zero degenerate triangles.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 2ddc849 to 3a0832e Compare September 2, 2026 13:13
Two real bugs reported after testing:

1. Merging layers behaved like Figma's Exclude instead of Union: it
   naively concatenated each source shape's outline and rendered with
   fillRule=evenodd, so any area where two shapes overlapped canceled out
   into a hole instead of filling solid. Added a real polygon boolean
   union (via polygon-clipping) so overlapping shapes now combine into one
   solid blob, matching what "merge" actually means.

2. Layers had no Z position at all — every shape extruded from Z=0, so
   increasing one layer's depth made it grow through/inside whatever else
   was also sitting at Z=0 instead of sitting on top of it. Added a `z`
   field to the transform (always clamped >= 0, so nothing can go through
   the print bed), wired through the 3D scene graph and world-transform
   math, a Z field + "Drop to bed" button in the inspector, and an
   Auto-Stack action that stacks every layer bottom-to-top with no overlap
   in one click.

Verified in a real browser: merging two overlapping circles now produces
a single solid contour (1 subpath, no hole) instead of 2 separate
subpaths that cancelled out; auto-stacking four layers with different
depths (2, 5, 1.2×4mm) produced an exported STL whose Z bounding box was
exactly 0-11.8mm, matching the expected cumulative sum.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 3a0832e to 465f7f3 Compare September 2, 2026 13:34
Two more bugs found after testing:

1. The Z field was invisible: CSS grid's 1fr tracks default to an "auto"
   minimum equal to their content's min-content size, and a bare
   <input type="number">'s browser-default intrinsic width (~170px) is
   wider than a 3-column track in the 240px sidebar. The 3rd column (Z)
   was being rendered ~120px past the sidebar's right edge, off-screen,
   with no horizontal scrollbar to reveal it. Same issue affected the
   print bed's Width/Depth/Height row. Fixed by letting grid tracks (and
   their item wrappers) shrink below their content's natural size.

2. Auto-Stack raised every layer's Z cumulatively in paint order
   regardless of whether it actually overlapped anything in X/Y, so two
   unrelated shapes on the same background (e.g. separate letters) ended
   up floating on top of each other instead of resting at the same
   height. Fixed: each layer's Z is now the tallest top-Z among only the
   previously-placed layers whose bounding box actually overlaps it.

Verified in a real browser: X/Y/Z fields and the bed size row now fit
without overflow (confirmed via computed bounding boxes, not just a
screenshot). Re-ran auto-stack on the sample badge and checked every
layer's resulting Z by hand against its actual overlaps — a background,
an unrelated circle and letter sitting at the same height on top of it,
and two crossing rects correctly stacking on the one they cross — then
confirmed the exported STL's Z bounding box (0-3.6mm) matches exactly.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 465f7f3 to 1ccbb81 Compare September 2, 2026 14:16
selection outline, shape tools, space-pan

Six requested items:

1. Fixed pinch-zoom hijacking the whole browser page. React attaches
   wheel listeners as passive by default, so e.preventDefault() inside
   the old onWheel prop was silently ignored — the browser's own
   pinch-zoom kept firing alongside ours. Replaced it with a native,
   non-passive wheel listener, and while at it split the gesture the way
   Figma does: plain scroll/two-finger swipe now pans, Cmd/Ctrl+scroll or
   pinch zooms (previously all wheel input was treated as zoom, which is
   also why panning felt wrong).

2. Fixed number fields that couldn't be cleared to type a new value (e.g.
   replacing "1" with "30"). They were plain controlled inputs bound
   straight to store state: clearing the field produced an invalid empty
   string, which got silently rejected, so React immediately snapped the
   DOM value back to the old number. NumberField now buffers the raw text
   locally while focused and only reconciles with the committed value on
   blur — applied everywhere a number field exists.

3. Added a Wireframe toggle in the 3D toolbar to see through solid
   surfaces and check nested/occluded geometry.

4. Added a real selected-object highlight in the 3D view: previously
   nothing indicated selection there at all. Tried the standard "enlarged
   backface-only shell" outline trick first, but our print pieces are
   flat and viewed mostly from above, where the dominant visible surface
   is the top face — so BackSide culls exactly the part that needs to
   show. Ended up with a same-height, XY-widened duplicate resolved with
   polygonOffset (the standard fix for coplanar surfaces) so the original
   always wins inside its own footprint and the widened rim shows past
   it — verified as a clean colored margin around the selected shape,
   with no relation to (and not to be confused with) a separate,
   pre-existing z-fighting artifact between unstacked overlapping layers
   that surfaced during testing.

5. Added a Figma-style floating shape toolbar (bottom of the 2D canvas)
   to add a rectangle or circle centered on the artboard. Text isn't
   included yet — turning a font into printable outlines is a
   meaningfully bigger feature on its own.

6. Space+drag now pans the 3D camera instead of orbiting, matching the
   2D canvas and Figma's own convention, by swapping OrbitControls' left-
   mouse-button binding while space is held.

Verified in a real browser: dispatched synthetic ctrlKey/non-ctrlKey wheel
events and confirmed one zooms while the other purely pans (position
changes, size doesn't) and both call preventDefault; walked through
clear-and-retype on a number field end to end; confirmed the auto-stack
+ 3D selection outline together with a screenshot showing a clean purple
outline around a selected shape once it was no longer coplanar with its
neighbor; wireframe toggle screenshot; space+drag smoke test with no
page scroll or console errors.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 1ccbb81 to 2594f04 Compare September 2, 2026 15:02
…adius fix, and edge bevel (#6)

- Grid/Wireframe toolbar buttons now use real toggle-button styling.
- Replace the 3D selection outline with a Figma-style bounding-box
  wireframe plus 8 corner handles, and add click-to-select in the 3D
  viewport (with shift/cmd/ctrl for additive selection, a drag threshold
  so orbiting doesn't select, and click-empty-space-to-deselect).
- Fix corner-radius (and color/extrusion) editing on imported SVG layers:
  selecting a group showed no shape-property sections at all, since every
  top-level imported layer is a group. Selecting a group now shows
  batch-edit versions that apply to every shape inside it.
- Add an independent top/bottom edge bevel (chamfer) — a new, simpler
  Z-axis edge treatment distinct from the existing 2D corner-radius
  fillet, with its own single-slider-per-edge UI. Implemented as a
  purpose-built extrude geometry builder (bevelExtrude.ts) rather than
  three.js's own ExtrudeGeometry bevel option, since that bevel is always
  symmetric and expands outward rather than chamfering inward.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 2594f04 to 9893bc6 Compare September 2, 2026 15:25
…vel (#7)

- Grid and Wireframe are now actual switch controls (pill track + sliding
  thumb), not just buttons that change color when active.
- The edge bevel now builds a proper rounded (quarter-round) fillet
  subdivided into multiple facets, instead of a single flat 45°-style cut.
  A single facet reads as an obviously faceted "cut corner"; enough small
  facets approximating a curve reads as a smooth, rounded edge — the same
  "Segments" lever a 3D modeler (e.g. Blender's Bevel modifier) would
  reach for. Same slider, no new UI — only the underlying geometry changed.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 9893bc6 to 0d016ed Compare September 2, 2026 16:30
* Fix concave bottom bevel and 3D selection losing to overlapping large objects

- The bottom edge bevel traced a concave (scooped/cove) curve instead of a
  convex round-over, because its arc was centered on an endpoint instead of
  on the sharp corner it replaces. Verified both algebraically (compared
  against the straight-chamfer baseline at several points along the curve)
  and visually — the bottom now matches the top's round-over shape.
- Fix 3D click-to-select losing to a larger overlapping/coplanar object:
  two root causes. (1) The Figma-style selection handles/edges added in the
  previous release had no raycast exclusion, so a click landing on one
  (which has no layerId) silently no-op'd instead of reaching the mesh
  behind it. (2) Raw depth-sorted raycasting has no tiebreaker for two
  meshes at the exact same distance (e.g. two shapes sitting flush at the
  same Z, before Auto-Stack separates them) — R3F's default "closest hit"
  resolution ends up stably favoring whichever mesh the scene graph
  happens to traverse first, which in practice is almost always the
  oldest/background layer. Selection now inspects every intersection
  within a small depth-tie band and, only among those, prefers the one
  painted latest — mirroring how the 2D canvas already resolves overlapping
  clicks (later-added shapes sit on top) — while distinctly-different
  depths still resolve by real distance as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

* Add hole/negative-space shapes with real 3D boolean subtraction

Adds a "Use as hole" toggle to any shape layer: instead of printing as its
own solid, its extruded volume is subtracted (a genuine 3D boolean
difference, via three-bvh-csg) from every solid it overlaps — for magnet
wells, screw holes, and similar cut-in features. The hole's own Z position
and depth are independent of the solid's, so it can be a shallow pocket or
punch all the way through.

- New `isHole` flag on ShapeLayer, with a real switch toggle in the
  Inspector's new "Negative space" section (reusing the toggle-switch
  component from the toolbar, now shared via ToggleSwitch.tsx).
- Core cutting logic lives in geometry/holeSubtraction.ts: after the normal
  scene graph is built, every solid mesh overlapping (by world-space
  bounding box) a hole mesh has the hole's volume subtracted via
  three-bvh-csg's Evaluator, chaining multiple overlapping holes against
  the same solid in sequence. The CSG library's own frame-of-reference
  convention (the result comes back in brush A's frame) means the result
  can be dropped in with the solid's exact original world transform, no
  extra local/world conversion needed.
- Hole shapes are shown as a translucent red overlay in the 3D preview
  (both solid-shaded and, distinctly colored, in wireframe) so the
  negative space stays visible while editing, and as a dashed
  red-translucent overlay in the 2D canvas — but the overlay is preview
  only: STL export always omits it, since by then its volume has already
  been cut from whatever it overlapped.

Verified: a plate with a hole shows a real cavity with visible depth (not
just a flat colored circle) in the 3D preview; wireframe shows the hole in
a clearly distinct color; the exported STL's Z bounding box tops out at
the plate's own depth (not the taller hole cutter), confirming the hole
never appears as separate printable geometry while the real cut is baked
into the solid's mesh (768 triangles, zero NaN vertices, valid binary STL).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

---------

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 0fcfa46 to eb29d98 Compare September 2, 2026 23:07
#9)

Four requested additions:

- Align: Figma-style left/center/right and top/middle/bottom align buttons
  in the Inspector. A single selection aligns to the artboard; several
  align to each other's combined bounding box. Implemented as a pure
  translation on top of the existing world-transform math, so nested/
  rotated/scaled layers still land correctly.

- Group Selection (Cmd/Ctrl+G), distinct from the existing Merge/flatten
  (Cmd/Ctrl+E): nests the selection under a new group while keeping every
  shape independently editable, unlike Merge which destroys individual
  shape identity. Ungroup (Cmd/Ctrl+Shift+G) is the inverse. Both rebase
  each reparented layer's transform (position, rotation, and non-uniform
  scale) so nothing visibly moves — verified with an identical before/
  after screenshot.

- Units: the Units dropdown was previously disabled/mm-only. It's now a
  real display/input conversion layer (mm <-> cm/in) applied to every
  physical-length field (position, bed size, extrusion depth, corner
  radius, bevel amounts) — Scale and Rotation are untouched since they
  aren't lengths. All internal storage and geometry math (extrusion, STL
  export) stay in mm exactly as before; verified the mm<->in round-trip
  is exact (10in -> 254mm).

- Curve smoothness: corner-radius fillets were using only ~4 straight
  segments for a 90° corner (ARC_SEGMENTS_PER_HALF_TURN=8), which is
  genuinely low-poly in the exported STL too, not just on screen — an STL
  is a real polygon mesh, so an under-tessellated curve prints faceted.
  Raised to 32 (~16 segments per 90°) and bumped the shape-tool circle
  from 48 to 64 segments; verified visually that curves are now smooth
  even zoomed in close, matching what will actually come off the printer.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from eb29d98 to c269ed3 Compare September 3, 2026 05:40
Holes (e.g. for embedding magnets) are usually sunk almost to the bed
with just a thin floor left, not punched straight through. Adds a
"Snap to recessed pocket" action that repositions a hole to leave a
configurable floor thickness above whatever it overlaps (defaulting
to the ~0.6mm/3-layer recommendation) while still cutting fully
through the top, plus diameter/thickness presets (common magnet
sizes, ISO 273 screw clearance holes) so users can pick a standard
size instead of hand-tuning scale and depth each time.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from c269ed3 to 9a3d145 Compare September 3, 2026 08:06
…ion (#11)

Any "apply to all" edit — the multi-select Color swatch, or any property
edited while a group is selected (Color, Extrusion, Corners, Negative
space, the new hole presets/snap) — called its store setter once per
shape via forEach, and each call was its own tracked history entry. A
single Ctrl+Z then only reverted the last shape touched, and since the
Inspector's fields mirror the first target rather than the last, that
one undo usually looked like it did nothing at all.

Wraps every such forEach in a shared applyToAll() helper that reuses
the existing pause/mutate/resume gesture (already used for slider
drags) so the whole batch collapses into exactly one undo step,
matching how a single-shape edit already behaves. The three range
sliders (corner radius, bevel top/bottom) are left untouched — they
already bundle correctly via their own pointerdown/pointerup gesture.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 9a3d145 to 11c5dda Compare September 3, 2026 09:43
1. Color swatch drag still broke undo: the native color picker fires
   many `input` events while you drag inside it (React's onChange is
   bound to that, not the once-only `change` event), and each one was
   still its own tracked history entry even after the earlier batch-undo
   fix. A single Ctrl+Z only undid the last, usually-imperceptible
   increment, which is exactly what looked like "undo doesn't work" for
   a whole-group recolor. Added ColorSwatchInput, which bundles the
   entire pick gesture (pointerdown through the native `change` event)
   into one undo step, the same way the range sliders already do for a
   drag.

2. Added a Figma-style cursor/select tool icon to the bottom toolbar
   (highlighted whenever nothing is selected), and real marquee
   (rubber-band) selection: dragging over empty canvas now draws a
   selection box and selects every top-level shape/group it overlaps,
   instead of only ever being able to select multiple objects one
   shift-click at a time. Since drag-on-empty-canvas is now marquee
   select, panning moved to Space+drag (matching the 3D viewport's own
   convention) alongside the existing scroll/trackpad-swipe pan.

3. Ctrl/Cmd+scroll and pinch-zoom sensitivity was tuned so low a single
   wheel notch or a full pinch gesture barely changed the zoom level.
   Raised it 3x, and added Figma's Z (zoom in) / Option+Z (zoom out)
   keyboard shortcuts alongside the universal +/- convention.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 11c5dda to 739626d Compare September 3, 2026 11:02
1. Selection outline was invisible for any plain, unrotated shape: its
   1px non-scaling-stroke sat exactly coincident with the shape's own
   fill edge (an accent-blue outline against an indigo fill), so it
   anti-aliased into nothing. A circle's square bounding box happened to
   show because its corners poke past the round fill into bare
   background — which is what made marquee-selecting two shapes look
   like "only one object selected." Outset the outline a hair past the
   shape's edge and widened it to 2px so it's unambiguously visible
   regardless of what's under it.

2. The hex color text field committed every raw keystroke straight to
   the layer, so clearing it to retype, or a partial/invalid hex typed
   along the way, applied garbage colors mid-type. HexColorInput now
   buffers the raw typed text (matching NumberField's existing pattern)
   and only commits once it's a complete, valid hex color, reconciling
   back to the real value on blur.

3. Z-zoom had two real bugs: the keydown handler had no e.repeat guard,
   so just holding Z let the OS's own key-repeat fire dozens of zoom
   steps a second, compounding into a runaway zoom in well under a
   second. Replaced it with Figma's actual behavior — holding Z (Option
   +Z to zoom out) swaps the cursor to a magnifying glass and does
   nothing by itself; a click on the artboard zooms one step centered
   on exactly where you clicked. (Found and fixed a second bug in the
   same change: a click landing on a shape bubbled the event up to the
   canvas's own handler too, applying the zoom twice.) Also added
   Cmd/Ctrl+0 to reset to a true 100%.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 739626d to a3735d7 Compare September 3, 2026 11:29
…ets (#14)

1. Dragging a shape that's inside a currently-selected group moved just
   that one shape, silently narrowing the selection down from the whole
   group — every canvas drag started from the raw shape under the
   cursor, and a group was never itself a draggable target. Clicking any
   member of a group now resolves to its outermost group ancestor first
   (new getTopLevelId), so the whole group selects and moves together,
   matching how Figma treats a click on a group's member as a click on
   the group unless you've drilled in.

2. The Z-zoom tool could get stuck armed (magnifier cursor + zoom-on
   -click) even after both keys were released. Root cause: the keyup
   handler matched e.key === "z"/"Z", but macOS remaps the character a
   held Option produces (e.g. to "Ω") — so releasing Z while Option was
   still down never matched, and zoomToolArmed never cleared. Switched
   to e.code (the physical key, immune to modifier remapping), the same
   fix already used for Space elsewhere in this file. Also fixed a
   layout bug (`right: 0` doesn't right-align in this container) that
   the hue slider crossed while investigating.

3. The hole diameter/thickness preset dropdowns always snapped back to
   "Common sizes…" after picking one, giving no visible confirmation of
   what was actually applied. They now show the picked size.

4. Replaced the OS's native color-wheel dialog with a real in-app
   Figma-style picker: a saturation/value square, a hue strip, and a hex
   field in a popover anchored off the swatch. Positioned as position:
   fixed with the trigger's screen rect measured and clamped to the
   viewport on open — a plain CSS-anchored popover either spilled off
   the browser edge or got clipped by the Inspector panel's own scroll
   clipping, depending on which swatch (the single-shape Color row vs.
   the multi-select "apply to all" row) opened it. The whole open-to
   -close session still collapses into one undo step via the existing
   gesture pattern.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from a3735d7 to b2873fb Compare September 3, 2026 11:53
…ts (#15)

1. Restored the format-switcher dropdown (Hex/RGB/CSS/HSL/HSB) in the
   color picker that the previous Figma-style rebuild had dropped in
   favor of just Hex — the request was for the full Figma dropdown, not
   a subset. Editing any format's fields updates the same underlying
   color and stays inside the picker's existing one-undo-step-per-session
   gesture. No alpha field: shape colors are plain opaque hex, there's no
   opacity channel in the data model to edit.

2. Clicking on a group's member now steps one level deeper into nested
   sub-groups each time you click it again (through as many levels as
   there are, down to the leaf shape) — Figma's "click to select the
   group, click again to work on what's inside it," generalized past a
   single level. Getting this right without breaking last round's "drag
   moves the whole group" fix took deferring the decision to pointerup:
   a click that lands within the already-selected group/shape keeps that
   selection as-is for the gesture, and ONLY resolves as a "step deeper"
   click if the pointer turns out not to have moved at all — otherwise a
   click that turned into a drag would drill in immediately instead of
   moving the whole thing, reintroducing the exact bug fixed last round.

3. Added the Bambu Lab models missing from the bed-size presets: X2D,
   P2S, A2L, and H2C, alongside the existing X1/X1E/P1S/P1P/A1/A1 mini/
   H2D (kept for anyone with existing documents sized to them).


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from b2873fb to 99eb115 Compare September 3, 2026 12:09
… groups (#16)

1. Merging shapes inside a group that itself had any position/rotation/
   scale bakes each source's points into world/absolute space for the
   union math, then stores the result with an identity transform of its
   own — correct only when the merged shape has no parent. As a CHILD of
   the group, that same parent transform gets applied a second time at
   render, visibly shifting the merged shape away from where the sources
   actually were. Added invertTransform2D (the inverse of the existing
   applyTransform2D) and, when the merge target has a parent, re-express
   the unioned points relative to that parent instead of leaving them in
   raw world space — the same "world space for the math, parent-local for
   storage" pattern rebaseWorldToParent already uses for reparenting.

2. Shift-click always resolved to the outermost group (added by the
   "drag moves the whole group together" fix a couple of rounds back),
   so shift-clicking two individual children of the same group to build
   a custom multi-selection just toggled the parent group twice instead.
   rawId in this handler is always an individual leaf shape's own id
   already — only shapes have a pointer handler, a group is an invisible
   <g> wrapper — so shift-click now toggles exactly that shape, no
   top-level resolution at all (unlike a plain click, which still climbs
   to the group and drills back in on repeat clicks). Also guards against
   the resulting selection ever holding both a group and one of its own
   descendants at once (an ancestor already selected is dropped in favor
   of the more specific child), the same invariant merge/group/duplicate
   already enforce — that combination would double-apply a drag to the
   descendant.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 99eb115 to 1c38af3 Compare September 3, 2026 13:46
…, hole styling (#17)

* Add 3D multi-drag, toast feedback, copy/paste shortcuts, align guides, hole styling

- Viewport3D: shapes (single, multi-select, or grouped) can now be dragged
  in the 3D view without requiring grouping first, bundled into one undo
  step, using a Y=0 plane raycast mapped back to document x/y.
- Canvas2D: the always-on control hint is now a small info icon next to the
  zoom indicator, shown on demand instead of eating canvas space.
- New toast notifications (bottom-center, auto-dismissing) confirm merge,
  group/ungroup, duplicate, delete, copy/paste, import, and export actions.
- Global Cmd/Ctrl+C and Cmd/Ctrl+V shortcuts copy/paste the current
  selection from anywhere, not just the layer panel's duplicate button.
- Canvas2D: dragging shapes now shows dashed pink alignment guides when an
  edge or center lines up with another shape, distinct from the blue
  selection outline.
- LayerPanel: hole/negative-space layers are now color-coded red, matching
  their canvas styling.
- ShapeToolbar: a new hole tool button creates a shape pre-marked as
  negative space, matching the existing Inspector hole toggle's behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

* Add 3MF export with per-shape colors for slicer compatibility

STL is pure geometry with no color channel, so Bambu Studio (or any
slicer) has no way to know a shape's color from an STL export. Add a
3MF export option alongside STL: one file containing a separate object
per shape, each carrying its own color via the 3MF core spec's
basematerials/pindex, with every object's real position preserved (no
manual re-assembly needed like exporting several STLs would require).
Uses three.js's bundled fflate (already used internally by USDZExporter)
to build the ZIP container, so no new dependency is needed. The top
toolbar's single Export button is now a small dropdown offering STL or
3MF.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

---------

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from b9c2d48 to 17f13e2 Compare September 3, 2026 14:48
…tor (#18)

1. Canvas2D: a plain click-drag on a member of an existing multi-selection
   (built via shift-click or marquee) fell through to a fallback that
   always collapsed the selection down to just the clicked shape before
   the drag even started, so dragging one of several selected shapes
   silently deselected the rest instead of moving them together.
   Generalized the existing single-selection "click within the
   already-selected group keeps the selection for the drag" check to any
   selection size, matching what 3D already did.

2. autoStackLayers used a bounding-box overlap test as a proxy for "does
   this shape rest on that one," which false-positives whenever two
   shapes' rectangular bounds touch but their real outlines don't (e.g.
   two circles near the same corner) or only partially overlap — lifting
   a shape to sit on something it isn't actually resting on, leaving part
   of it floating in mid-air with nothing underneath. Switched to real
   polygon intersection (via polygon-clipping, already a dependency) for
   both the overlap test and a new support-coverage check: if a shape ends
   up raised but its own footprint isn't ~fully covered by whatever raised
   it, a toast now names it as possibly floating.

3. threemf.ts's color export never worked in Bambu Studio: its foreign-3MF
   color importer (per BambuStudio's own bbs_3mf.cpp source) only reads
   the Materials Extension's <m:colorgroup>/<m:color>, not the 3MF core
   spec's <basematerials>/displaycolor this exporter was emitting. Switched
   to m:colorgroup so colors actually carry over.

4. Inspector: the Corners and Edge bevel sections now collapse to just
   their title + a small "+" when unset (matching Figma's own pattern for
   optional properties), only expanding to their full slider/field rows
   once added (or if already non-zero) — clicking "+" sets a real starting
   value, "−" resets to 0 and collapses again — freeing vertical space so
   more of the panel fits without scrolling.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 17f13e2 to 6bc8b52 Compare September 3, 2026 18:56
… add boolean ops and bed-preset pin (#19)

- LayerPanel: dragging one of several selected layers into a folder only
  moved that single row, and drop-target feedback was a uniform highlighted
  border regardless of whether you'd land above/below/inside a row. Added
  moveLayers (replacing moveLayer) so the whole current selection moves
  together and reparenting preserves each shape's world position (it never
  did), plus distinct above/below/inside drop indicators (an insertion line
  vs. a filled row) matching Figma/VS Code.
- deleteSelection's toast counted only top-level selected items, so
  deleting one folder with several shapes inside it reported "Deleted 1
  layer". Now counts every layer actually removed, descendants included.
- Root-caused corner radius silently not rounding on imported SVG shapes:
  a closed SVG path stores an explicit point at both its start and end
  (our own contour representation already treats the list as an implicit
  cycle), and that leftover duplicate makes the corner at that index look
  like a zero-length edge, silently skipping it — hand-drawn shapes never
  had this duplicate, which is why only imported ones were affected.
  roundContour now dedupes consecutive coincident points (including the
  wraparound pair) before rounding.
- Edge bevel's per-vertex offset gives a sharp polygon corner exactly one
  mitered point to sweep the whole round-over curve through, instead of a
  real arc (three.js's own ExtrudeGeometry bevel has this same limitation).
  Pre-rounds each corner into a short arc (sized off the bevel amount, via
  the same roundContour used for the shape's own corner-radius) before
  computing bevel movement vectors, so corners get several offset
  directions to sweep through instead of one.
- threemf.ts now filters degenerate/zero-area triangles and non-finite
  vertices instead of exporting them verbatim, and skips an object
  entirely rather than emitting an empty (spec-invalid) one — hardening
  against the geometry edge cases (bevel/CSG seams) that could produce a
  3MF a stricter slicer parser rejects.
- Auto-Stack's floating-shape warning now auto-selects the affected shapes
  (visible immediately on canvas) and posts as a persistent, click-to
  -dismiss toast instead of a 2.6s auto-vanishing one.
- Added a Figma-style Boolean ops dropdown (Union/Subtract/Intersect/
  Exclude) next to Group Selection for multi-selections, backed by real
  polygon-clipping difference/intersection/xor operations alongside the
  existing union-based Merge.
- Added a bookmark toggle next to the bed-size preset picker to pin a
  default Bambu printer bed size that new projects (and the app itself, on
  load) always open with, instead of always defaulting to the X1 Carbon.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 6bc8b52 to 45c9989 Compare September 3, 2026 19:41
… floating-shape warning (#20)

- Bed preset dropdown was matching by width/depth, and six Bambu presets
  share identical 256x256x256 dimensions, so it always snapped back to
  whichever came first (X1 Carbon). Match on bed.name first instead.
- Selecting a bed preset now auto-matches the artboard to it in one step;
  removed the now-redundant "Match print bed size" button.
- Reordering layers in the layer panel bubbled its drop event up to the
  app-level OS-file-drop handler, popping a spurious "Drop a .svg file to
  import it" error. Stop propagation on the row's own drop handler.
- Space+drag now pans even when the pointer comes down on a shape (the
  shape's own pointerdown handler ran first and never deferred to the
  canvas-level pan check).
- Removed "Fit artboard to selection" (both instances).
- Boolean ops (Union/Subtract/Intersect/Exclude) are now an always-visible
  2x2 button row instead of a dropdown.
- Added a persistent, debounced banner that flags shapes with no real
  support underneath them (independent of auto-stack) with Select/Fix
  actions; fixing respects layer stacking order so raising a background
  shape doesn't invert it on top of what it's supposed to support.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from 45c9989 to b3ff24c Compare September 3, 2026 20:20
ayhanars and others added 2 commits September 3, 2026 23:04
)

defaultDocument() correctly set document.bed to the pinned preset, but
widthMM/heightMM were still hardcoded to the plain 100x100 default —
so opening a fresh project carried the right printer but the wrong-size
artboard, only "half" honoring the pin. Size the artboard to the pinned
preset's dimensions too, same as any other bed-preset selection does.


Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC

Co-authored-by: Claude <noreply@anthropic.com>
A project can now hold several print-bed "plates" instead of just one
artboard's worth of objects — for when a set of pieces don't fit together
on one plate, or should just print as separate jobs, without having to
start a whole new project for the overflow.

- Plate tabs (bottom-left of the canvas) to switch, add, rename, and
  delete plates. All plates share the project's single bed/printer size.
- The 2D canvas, 3D viewport, and layer panel now only show the active
  plate's objects; drag a row from the layer panel onto a tab to move it
  there.
- Auto-Stack, the floating-shape warning, and Select All are scoped to
  the active plate only — a different plate is a different physical bed,
  so its objects have nothing to do with these.
- Export STL/3MF now export the current plate; a new "Export All Plates"
  zips one file per non-empty plate.
- New objects (import, paste, draw, and anything a boolean op/merge/
  group/ungroup produces at the top level) land on whichever plate is
  currently active; deleting a plate deletes its objects with it
  (confirmed first when it isn't empty), and undo/redo covers plate
  structure the same as any other document edit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiHJref4EjcyprLsYyGBvC
@ayhanars
ayhanars force-pushed the claude/svg-3d-print-designer-motvtz branch from b3ff24c to 05721e9 Compare September 3, 2026 22:35
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.

2 participants