Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 15 additions & 15 deletions .github/workflows/publish-unsigned-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ on:
description: New prerelease tag; an existing release is never overwritten
required: true
type: string
default: v0.5.0-beta.20
default: v0.5.0-beta.21
preview_title:
description: Public prerelease title
required: true
type: string
default: Parlyn Engine v0.5.0 Beta 20 (unsigned)
default: Parlyn Engine v0.5.0 Beta 21 (unsigned)

permissions:
contents: write
Expand All @@ -24,8 +24,8 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
GH_TOKEN: ${{ github.token }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.20' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 20 (unsigned)' }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.21' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 21 (unsigned)' }}

steps:
- name: Checkout
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
throw "Expected exactly one Parlyn installer."
}

$portable = "release/Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip"
$portable = "release/Parlyn-Engine-Portable-0.5.0-beta.21-x64.zip"
if (-not (Test-Path -LiteralPath "release/win-unpacked/Parlyn Engine.exe")) {
throw "Packaged Parlyn executable was not found for the portable build."
}
Expand All @@ -72,14 +72,14 @@ jobs:
shell: pwsh
run: |
@"
# Parlyn Engine v0.5.0 Beta 20

This unsigned Windows beta fixes saved Undo/Redo disappearing after
switching project scenes. History is stored independently per scene.
It retains Beta 19's optional scene and world authoring bounds.
Beta 19 was not accepted due to the history defect; Beta 20 awaits
human Windows acceptance. Previously overwritten history cannot be
reconstructed. Older editors discard bounds on save.
# Parlyn Engine v0.5.0 Beta 21

This unsigned Windows beta adds optional Surface placement on Move
gizmo release. Meshes and billboards drop to a lower authored mesh
surface or the editor ground; one Undo reverses movement and placement.
It does not implement runtime physics, slope alignment or imported
terrain. Rotate, Scale, Inspector and Ground/End remain unchanged.
Beta 20 passed maintainer testing; Beta 21 awaits human acceptance.
The portable ZIP creates no Windows installation or uninstall entry and
remains the preferred unsigned test path.

Expand All @@ -96,7 +96,7 @@ jobs:
other public certificate provider.

Maintainer acceptance instructions:
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.21.md
"@ | Set-Content -LiteralPath release/PREVIEW-NOTES.md -Encoding utf8

- name: Publish GitHub pre-release
Expand All @@ -114,6 +114,6 @@ jobs:
--notes-file release/PREVIEW-NOTES.md `
--prerelease `
"$($installer[0].FullName)" `
"release/Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip" `
"release/Parlyn-Engine-Portable-0.5.0-beta.21-x64.zip" `
"release/latest.yml" `
"release/SHA256SUMS.txt"
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased — Foundation Stabilization

- Beta 21: optional persistent Surface toggle places the moved primary node
on a lower mesh support or editor ground when the Move gizmo is released.
- Combined placement and movement into one Undo/Redo step; excluded self,
selection and related hierarchy from support targets.
- Added ray/eligibility and actual editor commit regression tests.
- Recorded Beta 20 human acceptance and explicitly planned the later session
reopening and separate autosave-draft steps, preserving Discard semantics.

- Beta 20: fixed project scenes overwriting one shared history file. Saved
Undo/Redo now uses a separate hashed path per scene, with read-only legacy
fallback and unchanged stale-snapshot/size validation.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@

# Parlyn Engine

Beta 21 adds an optional **Surface** toggle in the Placement group: on Move
release, a mesh or billboard drops onto the nearest suitable lower Mesh3D face
or the existing editor ground. Movement plus placement is one Undo step. It
does not affect Rotate, Scale or Inspector edits and is not physics/collision.
Beta 21 awaits human Windows acceptance; Beta 20 passed maintainer testing.

Beta 20 fixes saved per-scene Undo/Redo history after a scene switch. It retains
Beta 19's optional scene/world bounds through **Bounds** in the View command
group: blue scene guides and amber world guides in world coordinates. They do
not restrict camera movement, objects or physics. Scene edits use Undo/Redo and
normal Save; world edits use explicit Save World Bounds. Beta 19 was not accepted
due to the history defect; Beta 20 human Windows acceptance is pending.
due to the history defect; Beta 20 subsequently passed human Windows acceptance.
Beta 18 passed the full maintainer editor regression test.

**Parlyn Engine** is a free and open-source game engine designed primarily for **modern 2.5D development**, while keeping real 3D available wherever a project benefits from it.
Expand Down Expand Up @@ -237,7 +243,7 @@ See [`docs/WINDOWS-DISTRIBUTION.md`](docs/WINDOWS-DISTRIBUTION.md) for signing,
verification and the maintainer acceptance test.

Phase 1 acceptance is complete. The current Phase 2 editor candidate uses the
repeatable [`v0.5.0-beta.20 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md).
repeatable [`v0.5.0-beta.21 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.21.md).

### Windows release integrity

Expand Down
4 changes: 3 additions & 1 deletion docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Parlyn Engine v0.5.0 Beta 20 — Maintainer Acceptance

Status: pending human Windows acceptance. Beta 19 was not accepted because
Status: passed human Windows acceptance on 2026-09-13, including saved per-scene
Undo/Redo, Bounds validation/persistence and additional editor regression tests.
Beta 19 was not accepted because
saving a second project scene overwrote the first scene's local history.
Beta 20 separates saved history by project-relative scene path.

Expand Down
39 changes: 39 additions & 0 deletions docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Parlyn Engine v0.5.0 Beta 21 — Maintainer Acceptance

Status: pending human Windows acceptance. Beta 20 passed the maintainer's
full Bounds/history re-test and additional editor regression tests on 2026-09-13.

Scope: optional downward authoring placement on Move-gizmo release only.
No collision/physics, slope alignment, Inspector snap or asset-import changes.
Surface supports currently mean authored Mesh3D objects, not sprite backdrops.

1. Locate the Surface icon in the Placement group (down arrow onto a sloped
line). Verify its tooltip, focus, pressed state and independent toggle.
In a fresh preference store it must be off.
2. With Surface off, Move an object above the floor. It stays where released.
3. Turn Surface on, Move it while above the floor: on release its visible
bottom rests at the existing editor ground Y=-1.55. It does not drop mid-drag.
4. Create a Mesh3D platform via Add Node, Scale it wider/flatter and place it
below another mesh or billboard. With Surface on, release Move above it:
the node rests on its nearest lower mesh top, not on the ground below it.
5. Move beyond the platform: placement falls back to the editor ground. A node
below the ground with no lower support must not be lifted automatically.
This downward tool does not resolve already-intersecting geometry.
6. Undo once restores the whole previous position; Redo restores the moved
and placed result. Save, switch A/B scenes, reopen and verify history.
7. Rotate and Scale do not drop objects; Inspector coordinate changes do not
drop objects. A click without changing the gizmo must not invoke placement.
8. Check scaled/rotated moving objects, 2.5D and 3D views and billboard placement.
Only the center-underfoot support is sampled; slope-normal alignment and
footprint-wide collision are not part of this foundation.
9. With multi-selection, only the existing primary-node Move behavior applies;
selected nodes and related hierarchy nodes must not act as self-support.
10. Transform Snap remains independent. Surface positioning takes precedence
over the final vertical grid increment. Ground button/End stays unchanged.
11. Toggle Surface, restart and confirm the preference is restored. Toggling
must not mark a scene dirty or add History. Re-test panels, Bounds, F,
Save/Open and unsaved Cancel/Discard/Save & Continue.

Unavailable, disabled, overhead or steep mesh faces are not support targets.
The downward ray is bounded to 1000000 authoring units. This is not runtime
collision detection or an imported-terrain placement promise.
20 changes: 17 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Completion evidence:

- duplication and reparenting — initial editor commands complete;
- durable, validated local Scene History — initial foundation complete;
Beta 20 corrects saved per-scene history isolation; human re-test pending;
Beta 20 saved per-scene history isolation passed human re-testing on 2026-09-13;
- multi-selection where appropriate — initial hierarchy and viewport selection complete;
- multi-scene project browser — validated listing, switching, creation, rename and move complete;
- rename and move project assets — initial safe editor workflow complete;
Expand All @@ -64,12 +64,26 @@ Completion evidence:
configurable snapping and explicit Local/World orientation;
- optional Surface/Ground Snap and Grid Snap, including a keyboard command to
place the current selection on the ground — initial Ground command and End
shortcut complete; automatic surface placement remains;
shortcut complete; optional Move-release surface placement implemented in
Beta 21, human acceptance pending;
- Frame Selected — initial single- and multi-selection camera framing complete;
- optional scene/world bounds independent from viewport size — initial validated
world-space authoring guides implemented in Beta 19; human acceptance pending;
world-space authoring guides passed human acceptance with Beta 20;
- consistent commands and keyboard behavior.

Explicitly accepted Phase 2 workspace follow-ups (planned, not parallel work):

1. Optional **Reopen Last Session** after the remaining placement and command
checks: reopen the last project/scene's saved state, then restore editor
view/camera/selection safely; unavailable files must fall back to normal startup.
2. Optional autosave to separate drafts, implemented and tested as its own step.
Normal Save commits scene data; Discard must discard the draft too. Autosave
must not silently overwrite scene files or restore intentionally discarded
changes. A minimal offer to reopen an interrupted draft belongs to this
design, not a broader recovery subsystem.

These follow-ups were explicitly approved by the maintainer on 2026-09-13.

## Phase 3 — 2.5D renderer and materials

**Status: Planned**
Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING-v0.5.0.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Parlyn Engine v0.5.0 Test Guide

For the current Phase 2 editor acceptance pass, use the shorter artifact-specific
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md)
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.21.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.21.md)
The guide below remains the detailed feature reference.

This test focuses on the two new foundations introduced in v0.5.0: viewport transform gizmos and the module lifecycle.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parlyn-engine",
"version": "0.5.0-beta.20",
"version": "0.5.0-beta.21",
"private": true,
"description": "Parlyn Engine - open-source 2.5D-first game engine editor",
"author": "Parlyn Engine contributors",
Expand Down
3 changes: 2 additions & 1 deletion scripts/check-desktop-boundaries.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const { resolveExistingProjectPath, resolveWritableProjectPath, resolveWritableP
assert.match(html, /class="tool-group viewport-tool-group" aria-label="Transform tools"/);
assert.match(html, /class="tool-group placement-tool-group" aria-label="Placement tools"/);
assert.match(html, /class="tool-group viewport-view-group" aria-label="Viewport mode"/);
assert.equal((html.match(/class="command-icon"/g) || []).length, 8, 'Command Bar must expose eight Parlyn command icons.');
assert.equal((html.match(/class="command-icon"/g) || []).length, 9, 'Command Bar must expose nine Parlyn command icons including Surface.');
assert.match(html, /id="surface-placement"[^>]*aria-pressed="false"/);
assert.match(html, /id="tool-select"[^>]+aria-label="Select"/);
assert.match(html, /id="tool-move"[^>]+aria-label="Move"/);
assert.match(html, /id="tool-rotate"[^>]+aria-label="Rotate"/);
Expand Down
65 changes: 65 additions & 0 deletions scripts/check-surface-placement.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
import * as THREE from 'three';
import { SceneDocument } from '../src/engine/scene/SceneDocument.mjs';
import { Node3D } from '../src/engine/core/Node3D.mjs';
import { Node2_5D } from '../src/engine/core/Node2_5D.mjs';
import { ThreeRenderer } from '../src/engine/render/ThreeRenderer.mjs';
import { History } from '../src/engine/history/History.mjs';
import { placeNodeOnSurface } from '../src/engine/editor/SurfacePlacement.mjs';

const scene = new SceneDocument();
const base = scene.root.addChild(new Node3D({ type:'Mesh3D', position:{y:0}, scale:{x:4,y:0.5,z:4} }));
const upper = scene.root.addChild(new Node3D({ type:'Mesh3D', position:{y:2}, scale:{x:2,y:0.5,z:2} }));
const moving = scene.root.addChild(new Node3D({ type:'Mesh3D', position:{y:5} }));
const renderer = new ThreeRenderer(null); renderer.scene = new THREE.Scene(); renderer.rebuild(scene);
assert.equal(renderer.getSurfacePlacedPosition(moving.id).y, 3.125, 'Nearest lower platform must win.');
assert.equal(renderer.getSurfacePlacedPosition(moving.id, [upper.id]).y, 1.125);
const beforeQuery = scene.toJSON();
renderer.getSurfacePlacedPosition(moving.id);
assert.deepEqual(scene.toJSON(), beforeQuery, 'Query must not mutate documents.');
moving.position.x = 20; renderer.updateNodeTransform(moving);
assert.ok(Math.abs(renderer.getSurfacePlacedPosition(moving.id).y + 0.8) < 0.000001, 'Missing mesh support uses the editor ground.');
moving.position.y = -5; renderer.updateNodeTransform(moving);
assert.equal(renderer.getSurfacePlacedPosition(moving.id), null, 'No upward placement from below ground.');
assert.equal(renderer.getSurfacePlacedPosition('missing'), null);
moving.position = {x:0,y:5,z:0}; upper.position.y = 8; renderer.updateNodeTransform(moving); renderer.updateNodeTransform(upper);
assert.equal(renderer.getSurfacePlacedPosition(moving.id).y, 1.125, 'Overhead platforms must not lift the node.');
upper.position.y = 2;
moving.position = {x:0,y:5,z:0};
upper.enabled = false; renderer.rebuild(scene);
assert.equal(renderer.getSurfacePlacedPosition(moving.id).y, 1.125, 'Disabled supports are excluded.');
upper.enabled = true;
const sprite = scene.root.addChild(new Node2_5D({ type:'Billboard2_5D', position:{x:0,y:6,z:0} }));
sprite.metadata = {width:2,height:2}; renderer.rebuild(scene);
assert.equal(renderer.getSurfacePlacedPosition(sprite.id).y, 3.375, 'Billboards can be placed on mesh surfaces.');
assert.equal(placeNodeOnSurface(scene, renderer, sprite.id), true);
assert.equal(placeNodeOnSurface(scene, renderer, sprite.id), false, 'Already placed nodes are a no-op.');
assert.equal(placeNodeOnSurface(scene, renderer, scene.root.id), false);

// Execute the actual editor commit function to verify opt-out and one-step Undo.
const source = fs.readFileSync(new URL('../src/renderer/app.mjs', import.meta.url), 'utf8');
const start = source.indexOf('function commitGizmoTransform(id, transform)');
const end = source.indexOf('function transformModeLabel', start);
assert.ok(start >= 0 && end > start);
for (const enabled of [false, true]) for (const mode of ['translate', 'rotate', 'scale']) {
moving.position = {x:0,y:5,z:0}; renderer.rebuild(scene);
const before = scene.toJSON(); const history = new History();
const context = {scene, renderer, gizmoStartSnapshot:before, transformMode:mode,
surfacePlacementEnabled:enabled, selected:null, selectedIds:new Set([moving.id]),
placeNodeOnSurface, populateInspector(){}, sceneSnapshot:()=>scene.toJSON(),
applyGizmoTransform(id, transform){ moving.position = {...transform.position}; renderer.updateNodeTransform(moving); },
pushHistory(snapshot,label){ history.push(snapshot,label); },
transformModeLabel:()=>mode, status:{textContent:''} };
const commit = vm.runInNewContext('(' + source.slice(start,end).trim() + ')', context);
commit(moving.id, {position:{x:0.2,y:5,z:0}});
assert.equal(moving.position.y, enabled && mode === 'translate' ? 3.125 : 5);
assert.equal(history.undoStack.length, 1, 'Move and placement must be one history entry.');
assert.deepEqual(history.undo(scene.toJSON()).snapshot, before);
}
// Hierarchy exclusions must not inadvertently exclude unrelated sibling supports.
moving.position = {x:0,y:5,z:0}; renderer.rebuild(scene);
assert.equal(placeNodeOnSurface(scene, renderer, moving.id, [moving.id]), true);
assert.equal(moving.position.y, 3.125);
console.log('Optional Move-release surface placement checks passed.');
2 changes: 1 addition & 1 deletion scripts/check-windows-distribution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ requireValue(workflow.includes('Verify signed Windows artifacts'), 'Signed outpu
requireValue(workflow.includes('require_signing'), 'Windows workflow must distinguish signed and unsigned preflight builds.');
requireValue(!workflow.includes('BEGIN PRIVATE KEY'), 'Signing material must never be embedded in the workflow.');
requireValue(previewWorkflow.includes('Compress-Archive'), 'Unsigned previews must create a portable ZIP test artifact.');
requireValue(previewWorkflow.includes('Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip'), 'Portable preview artifact has an unexpected name.');
requireValue(previewWorkflow.includes('Parlyn-Engine-Portable-0.5.0-beta.21-x64.zip'), 'Portable preview artifact has an unexpected name.');
requireValue(previewWorkflow.includes('release/win-unpacked/Parlyn Engine.exe'), 'Portable preview must verify its packaged executable.');

const gitignore = fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
Expand Down
Loading
Loading