13 KiB
Phase 2 — Architectural Specification
Overview
Phase 2 adds interactive vertex editing (custom shapes), per-panel zoom, and an expanded .stk save format to the Stickman Studio editor. Phase 1 (shape creation, save/load, whole-stickman assembly) is complete and functional; Phase 2 builds on it without breaking existing behavior.
1. Data Model Changes
1a. Shape Data Dictionary (updated)
# body_part_panel.gd — internal shape_data extends to:
{
"shape_type": String, # "line", "rectangle", "circle", "" (unchanged)
"points": PackedVector2Array, # vertex positions (unchanged)
"color": String, # hex color (unchanged)
"closed": bool, # NEW — true for polygon fill, false for open line
"vertex_flags": PackedInt32Array # NEW — 0=original, 1=user_created; same length as points
}
closedreplaces the implicit closed-ness that was derived fromshape_type. A line hasclosed=false; a rectangle and circle haveclosed=true. The rendering logic switches onclosedinstead ofshape_type.vertex_flagstracks which vertices are "original" (filled circle handle) vs "user-created via Create Point" (hollow rectangle handle). Default all-zero for starter shapes.shape_typeis retained as a descriptive tag but no longer drives rendering behavior.
1b. .stk File Format (version "1.1")
{
"version": "1.1",
"stickman_name": "Bob",
"body_parts": {
"head": {
"shape_type": "circle",
"closed": true,
"points": [{"x": 120, "y": 40}, ...],
"color": "#000000",
"position": {"x": 150, "y": 40},
"vertex_flags": [0, 0, 0, 1, 0, ...]
}
},
"metadata": {
"created_at": "...",
"modified_at": "..."
}
}
- Version bumped to
"1.1". Loading: if version is"1.0", auto-migrate (setclosedbased onshape_type, setvertex_flagsas all-zeros). Save always writes"1.1". - New per-shape keys:
"closed"(bool) and"vertex_flags"(array of ints, length matchingpoints). - Backward compatibility: loading a 1.0 file infers
closedfromshape_type("line"→false,"rectangle"/"circle"→true,""→false). vertex_flags defaults to all zeros for any shape loaded from 1.0.
1c. Circle Refactor
_create_circle()generates 12 vertices (down from 32). The shape_type remains"circle",closed=true,vertex_flagsall zeros.- Existing 32-vertex circles in saved
.stkfiles still load correctly —set_shape_data()accepts arbitrary point arrays.
2. BodyPartPanel — Vertex Editing
2a. Selection State
New internal state variables in body_part_panel.gd:
var _selected: bool = false # is this panel's shape currently selected?
var _dragging_vertex: int = -1 # index of vertex being dragged, -1 if none
var _drag_offset: Vector2 # offset from vertex to mouse during drag
var _zoom: float = 1.0 # zoom factor clamped to [0.3, 3.0]
2b. Interaction Model
| Action | Trigger | Behavior |
|---|---|---|
| Select shape | Left-click inside shape bounds | Sets _selected=true, emits shape_selected signal. Deselects all other panels (coordinated by stickman_editor.gd). |
| Deselect | Left-click outside shape OR clicking another panel | _selected=false. Any in-progress drag is cancelled. |
| Drag vertex | Left-click press on a vertex handle, then drag | Sets _dragging_vertex to that index. On mouse motion, updates vertex position. On release, clears drag state. Emits shape_changed. |
| Open context menu | Right-click on the outline path of a selected shape | Shows a PopupMenu with one item: "Create Point". The click position is stored for vertex insertion. |
| Create initial shape | Right-click on an empty drawing area (no shape OR shape not selected) | Shows the existing 3-item context menu (Line, Rectangle, Circle). Unchanged from Phase 1. |
2c. Hit Testing
- Shape selection hit-test: Point-in-polygon check against the full shape polygon. For open shapes (lines), check if the click is within a threshold distance (12px) of any edge.
- Vertex hit-test: For each vertex, check if the mouse position is within
HANDLE_RADIUS_SELECTION(8px) of the vertex position (in screen space, accounting for zoom). - Edge hit-test for Create Point: Find the nearest edge to the right-click position (in screen space). The new vertex is inserted at the midpoint of that edge.
2d. Rendering Changes (in _on_drawing_area_draw())
Rendering order (back to front):
- Fill (if
closed):draw_colored_polygon(pts, color)— same as Phase 1. - Outline:
_draw_polyline(pts, color, ...)— same as Phase 1. - Selection highlight (if
_selected): Redraw the polyline in white (Color.WHITE), 2px wide, on top of the color outline. This makes the selected shape stand out. - Vertex handles: For each vertex:
- If
vertex_flags[i] == 0(original): filled circle, radiusHANDLE_RADIUS / zoom(so it stays constant screen size). - If
vertex_flags[i] == 1(user-created): hollow rectangle, size(6/zoom)×(6/zoom), centered on the vertex. - If
i == _dragging_vertex: draw in yellow highlight.
- If
2e. Context Menu Refactor
The existing ContextMenu (ids 0, 1, 2 — Line, Rectangle, Circle) is repurposed for initial shape creation only.
A second PopupMenu is needed for vertex editing. Two options:
- Option A (chosen): Single PopupMenu, dynamically repopulated based on context. When right-clicking on empty area → show "Line", "Rectangle", "Circle". When right-clicking on outline of selected shape → show "Create Point" only. This requires clearing/rebuilding the menu items programmatically.
- Option B: Two separate PopupMenu nodes.
Decision: Option A — less scene overhead, single menu node reused.
Menu id: 0=Line, 1=Rectangle, 2=Circle, 3=Create Point (only shown in vertex context).
2f. Vertex Insertion (Create Point)
- Right-click on the outline of a selected shape.
- Find the nearest edge (line segment) to the click position.
- Insert a new vertex at the midpoint of that edge (not at the click position — this matches the user story where the vertex "appear[s] in the outline where the user clicked" at the nearest edge midpoint, not an arbitrary point).
- Append
1tovertex_flagsat the corresponding position. queue_redraw()andemit shape_changed.
Note: PROJECT.md says "a small hollow rectangle will appear in the outline where the user clicked — this is not a point(vertex)". I interpret this to mean: the vertex is placed on the outline at the nearest edge midpoint. The "not a point" language means it's a vertex, just drawn differently (hollow rectangle).
2g. Vertex Dragging
- Left-click press on a vertex handle (within
HANDLE_RADIUS_SELECTION). - Set
_dragging_vertex = index,_drag_offset = position - vertex_position. - On mouse motion (
InputEventMouseMotion):- Compute new world-space position:
new_pos = mouse_position / zoom(undo zoom) or keep everything in world space. Design decision: Store points in world space (unscaled). Applydraw_set_transform(Vector2.ZERO, 0, Vector2(zoom, zoom))in_draw(). Mouse positions are divided by zoom to get world coordinates. - Update
shape_data.points[_dragging_vertex] = world_pos. queue_redraw().
- Compute new world-space position:
- On mouse release: clear
_dragging_vertex, emitshape_changed.
3. Window Zoom
3a. BodyPartPanel Zoom
- Add
_zoom: float = 1.0(range[0.3, 3.0]). - Mouse wheel handling in
_on_drawing_area_gui_input():MOUSE_BUTTON_WHEEL_UP:_zoom = clamp(_zoom * 1.10, 0.3, 3.0)MOUSE_BUTTON_WHEEL_DOWN:_zoom = clamp(_zoom / 1.10, 0.3, 3.0)
- In
_on_drawing_area_draw(), apply:drawing_area.draw_set_transform(Vector2.ZERO, 0.0, Vector2(_zoom, _zoom))before all draw calls. - Mouse-to-world conversion: All input event positions must be divided by
_zoombefore using as world coordinates (hit-testing, vertex creation, dragging). - Vertex handles drawn with sizes divided by
_zoom(e.g.,HANDLE_RADIUS / _zoom), so they appear the same screen size regardless of zoom level.
3b. WholeStickmanPreview Zoom
- Add
_zoom: float = 1.0(same range). - Same wheel handling, same
draw_set_transformapplication. - No vertex handles drawn here, so the handle-size concern doesn't apply.
- Drag hit-testing (
_try_start_drag): Divideat_positionby_zoomto convert to world coordinates. The drag offset and part positions remain in world space.
4. StickmanEditor — Coordination Changes
4a. Selection Coordination
When a BodyPartPanel is selected, all other panels must be deselected. Add:
signal shape_selected(part_name: String)
# In _on_body_part_shape_selected(part_name):
# for each panel in _body_part_panels where key != part_name:
# panel.deselect()
Add a deselect() method to BodyPartPanel that sets _selected=false and queue_redraw().
4b. Save Format Updates
FILE_VERSION→"1.1"._build_json_data()now includes"closed"and"vertex_flags"in each shape._collect_all_shape_data()updated accordingly —BodyPartPanel.get_shape_data()returns the updated dictionary withclosedandvertex_flags._apply_json_data()handles both"1.0"and"1.1":- 1.0: auto-migrate (infer
closedfromshape_type, setvertex_flagsto all-zeros). - 1.1: read
closedandvertex_flagsdirectly.
- 1.0: auto-migrate (infer
4c. New Signal Wiring
BodyPartPanel.shape_changed— existing, now also emitted on vertex drag end and Create Point.BodyPartPanel.shape_selected— new, for cross-panel deselection coordination.- Each panel's
shape_selectedconnects tostickman_editor._on_body_part_shape_selected.
5. WholeStickmanPreview — Changes
5a. Zoom Support
- Add
_zoomwith same wheel handling and range. - Apply
draw_set_transformin_on_preview_draw(). - Convert mouse positions in
_try_start_drag()and_on_preview_gui_input().
5b. Closed Shape Rendering
- The preview currently switches on
shape_typeto decide fill+close. Update to checkclosed: boolfrom the shape data instead. - Add
_draw_polyline_preview()helper for clean line drawing (consistent with BodyPartPanel).
6. Files Modified
| File | Changes |
|---|---|
scripts/body_part_panel.gd |
Major: vertex editing state, selection, zoom, context menu refactor, rendering with closed flag & vertex_flags, circle→12 vertices, mouse handling expanded |
scripts/stickman_editor.gd |
Version bump to 1.1, save format includes closed/vertex_flags, load handles 1.0+1.1, selection coordination signal |
scripts/whole_stickman_preview.gd |
Zoom support, closed-based rendering instead of shape_type-based, input coordinate transform |
scenes/body_part_panel.tscn |
Minimal — ContextMenu items may be reduced or left as-is (dynamic repopulation in script). Consider adding a 4th static item "Create Point" hidden by default. |
README.md |
Document Phase 2 features, updated .stk format, vertex editing, zoom |
docs/phase2_spec.md |
This file |
7. Implementation Order (Recommended)
- Data model + circle refactor (shape_data adds
closed+vertex_flags, circle→12 vertices, rendering usesclosed) - Zoom (mouse wheel, draw_set_transform, coordinate conversion, handle-size scaling)
- Shape selection (left-click hit-test, white outline, cross-panel deselection)
- Vertex hit-testing & dragging (left-click on handles, drag motion, release)
- Create Point (right-click context menu on selected outline, edge finding, vertex insertion)
- Save/load update (version 1.1, closed/vertex_flags in JSON, backward compatible loading)
- WholeStickmanPreview zoom + closed rendering
- README update
8. Edge Cases & Constraints
- Empty panel: Right-click shows shape creation menu (unchanged). Left-click does nothing. Zoom still works.
- Single-vertex shapes: If a shape somehow has only 1 vertex, skip rendering (guard already exists at
count < 2). - Min zoom (30%): Vertex handles drawn at
HANDLE_RADIUS / 0.3 ≈ 10px— still visible. - Max zoom (300%): Vertex handles at
HANDLE_RADIUS / 3.0 ≈ 1px— still visible but small. - Zoom state is NOT persisted in the .stk file. Each panel resets to 1.0 on load/clear (editor-only transient state).
- Vertex drag performance: Emit
shape_changedon drag end (not every frame) to avoid excessive WholeStickmanPreview rebuilds. Redraws during drag only touch the local panel. - Multiple selected shapes: Only one shape can be selected at a time across the entire editor.
- Right-click on empty area when shape exists but not selected: Shows shape creation menu (existing behavior), which replaces the current shape.