diff --git a/test/CoordinateSystem/README.md b/test/CoordinateSystem/README.md
new file mode 100644
index 000000000..28e393e3b
--- /dev/null
+++ b/test/CoordinateSystem/README.md
@@ -0,0 +1,30 @@
+# SPX coordinate-system regression demo
+
+This demo exercises the coordinate ownership introduced by:
+
+- goplus/spx#1702
+- goplus/godot#327
+- goplus/spx#1703
+
+Expected behavior:
+
+1. The stage center is SPX `(0, 0)`, `+X` points right, and `+Y` points up.
+2. The black cross is the sprite's logical root. Pressing `Space` cycles the
+ same SVG through three costume centers. The selected colored anchor must
+ stay on the black cross while the rendered SVG moves around it.
+ The SVG source dimensions are declared explicitly as `120 × 80`, so each
+ center is interpreted in the same source-image pixel coordinate space.
+3. `R` rotates around the logical root. `F` flips left/right around the same
+ root. `N` restores normal rotation.
+4. `P` draws a cyan pen segment in SPX `+Y`, so the segment must go upward.
+5. `S` stamps the current rendered costume and then moves the live sprite to
+ the right. The stamp must remain exactly where the rendered costume was.
+6. Arrow keys move both the logical root and its rendering together. `C`
+ resets the demo and `E` clears pen drawings and stamps.
+
+Run with the native SPX runtime:
+
+```sh
+cd test/CoordinateSystem
+spx runnative
+```
diff --git a/test/CoordinateSystem/RootMarker.spx b/test/CoordinateSystem/RootMarker.spx
new file mode 100644
index 000000000..c33bd7a9a
--- /dev/null
+++ b/test/CoordinateSystem/RootMarker.spx
@@ -0,0 +1,2 @@
+// The marker is a separate sprite at Subject's logical SPX position. It must
+// not inherit Subject's costume render offset, rotation, or horizontal flip.
diff --git a/test/CoordinateSystem/Subject.spx b/test/CoordinateSystem/Subject.spx
new file mode 100644
index 000000000..58e7bd0ac
--- /dev/null
+++ b/test/CoordinateSystem/Subject.spx
@@ -0,0 +1,143 @@
+import "fmt"
+import "math"
+
+const coordinateStep = 40.0
+
+var (
+ expectedX = 0.0
+ expectedY = 100.0
+ flipped bool
+)
+
+func syncLogicalRoot() {
+ RootMarker.SetXYpos xpos, ypos
+}
+
+func acceptLogicalPosition() {
+ // Motion can be clamped by the stage fence. Use the resulting logical SPX
+ // position as the baseline for later costume/rotation invariants.
+ expectedX, expectedY = xpos, ypos
+}
+
+func verifyLogicalPosition(action string) {
+ if math.Abs(xpos-expectedX) > 0.001 || math.Abs(ypos-expectedY) > 0.001 {
+ msg := fmt.Sprintf("FAIL %s: got (%.1f, %.1f), want (%.1f, %.1f)", action, xpos, ypos, expectedX, expectedY)
+ println msg
+ say msg
+ return
+ }
+ println fmt.Sprintf("PASS %s: logical SPX position = (%.1f, %.1f)", action, xpos, ypos)
+}
+
+func reportState(action string) {
+ verifyLogicalPosition action
+ syncLogicalRoot
+ say fmt.Sprintf("%s\nanchor=%s\nSPX=(%.0f, %.0f)", action, costumeName, xpos, ypos)
+}
+
+func resetCoordinateDemo() {
+ expectedX, expectedY = 0, 100
+ setXYpos expectedX, expectedY
+ setRotationStyle Normal
+ setHeading 90
+ setCostume 0
+ flipped = false
+ reportState "reset"
+}
+
+onStart => {
+ setPenSize 4
+ setPenColor HSB(52, 80, 90)
+ resetCoordinateDemo
+
+ // Exercise each asset-space center once. Only RenderRoot should move.
+ for i := 0; i < 3; i++ {
+ setCostume i
+ reportState "startup center conversion"
+ wait 0.8s
+ }
+ setCostume 0
+ reportState "ready"
+}
+
+onClick => {
+ setCostume Next
+ reportState "click: next costume center"
+}
+
+onKey KeySpace, => {
+ setCostume Next
+ reportState "next costume center"
+}
+
+onKey KeyUp, => {
+ changeYpos coordinateStep
+ acceptLogicalPosition
+ reportState "move +Y"
+}
+
+onKey KeyDown, => {
+ changeYpos -coordinateStep
+ acceptLogicalPosition
+ reportState "move -Y"
+}
+
+onKey KeyLeft, => {
+ changeXpos -coordinateStep
+ acceptLogicalPosition
+ reportState "move -X"
+}
+
+onKey KeyRight, => {
+ changeXpos coordinateStep
+ acceptLogicalPosition
+ reportState "move +X"
+}
+
+onKey KeyR, => {
+ setRotationStyle Normal
+ turn 30
+ reportState "rotate around logical root"
+}
+
+onKey KeyF, => {
+ setRotationStyle LeftRight
+ if flipped {
+ setHeading 90
+ } else {
+ setHeading -90
+ }
+ flipped = !flipped
+ reportState "flip around logical root"
+}
+
+onKey KeyN, => {
+ setRotationStyle Normal
+ setHeading 90
+ flipped = false
+ reportState "normal rotation"
+}
+
+onKey KeyP, => {
+ penDown
+ changeYpos 80
+ penUp
+ acceptLogicalPosition
+ reportState "pen moved +Y"
+}
+
+onKey KeyS, => {
+ stamp
+ changeXpos 140
+ acceptLogicalPosition
+ reportState "stamp then move +X"
+}
+
+onKey KeyC, => {
+ resetCoordinateDemo
+}
+
+onKey KeyE, => {
+ eraseAll
+ reportState "erase pen and stamps"
+}
diff --git a/test/CoordinateSystem/assets/coordinate-grid.svg b/test/CoordinateSystem/assets/coordinate-grid.svg
new file mode 100644
index 000000000..270f95c74
--- /dev/null
+++ b/test/CoordinateSystem/assets/coordinate-grid.svg
@@ -0,0 +1,42 @@
+
diff --git a/test/CoordinateSystem/assets/index.json b/test/CoordinateSystem/assets/index.json
new file mode 100644
index 000000000..76309492a
--- /dev/null
+++ b/test/CoordinateSystem/assets/index.json
@@ -0,0 +1,28 @@
+{
+ "run": {
+ "title": "SPX Coordinate System",
+ "width": 640,
+ "height": 480
+ },
+ "backdrops": [
+ {
+ "name": "coordinate-grid",
+ "path": "coordinate-grid.svg"
+ }
+ ],
+ "backdropIndex": 0,
+ "zorder": [
+ {
+ "type": "sprite",
+ "target": "Subject",
+ "x": 0,
+ "y": 100
+ },
+ {
+ "type": "sprite",
+ "target": "RootMarker",
+ "x": 0,
+ "y": 100
+ }
+ ]
+}
diff --git a/test/CoordinateSystem/assets/sprites/RootMarker/index.json b/test/CoordinateSystem/assets/sprites/RootMarker/index.json
new file mode 100644
index 000000000..aa71246af
--- /dev/null
+++ b/test/CoordinateSystem/assets/sprites/RootMarker/index.json
@@ -0,0 +1,21 @@
+{
+ "costumes": [
+ {
+ "bitmapResolution": 1,
+ "imageWidth": 24,
+ "imageHeight": 24,
+ "name": "logical-root",
+ "path": "root-marker.svg",
+ "x": 12,
+ "y": 12
+ }
+ ],
+ "costumeIndex": 0,
+ "heading": 90,
+ "isDraggable": false,
+ "rotationStyle": "none",
+ "size": 1,
+ "visible": true,
+ "x": 0,
+ "y": 100
+}
diff --git a/test/CoordinateSystem/assets/sprites/RootMarker/root-marker.svg b/test/CoordinateSystem/assets/sprites/RootMarker/root-marker.svg
new file mode 100644
index 000000000..ea4378ea1
--- /dev/null
+++ b/test/CoordinateSystem/assets/sprites/RootMarker/root-marker.svg
@@ -0,0 +1,5 @@
+
diff --git a/test/CoordinateSystem/assets/sprites/Subject/index.json b/test/CoordinateSystem/assets/sprites/Subject/index.json
new file mode 100644
index 000000000..563441507
--- /dev/null
+++ b/test/CoordinateSystem/assets/sprites/Subject/index.json
@@ -0,0 +1,42 @@
+{
+ "costumes": [
+ {
+ "bitmapResolution": 1,
+ "imageWidth": 120,
+ "imageHeight": 80,
+ "name": "geometric-center",
+ "path": "subject.svg",
+ "faceRight": 0,
+ "x": 60,
+ "y": 40
+ },
+ {
+ "bitmapResolution": 1,
+ "imageWidth": 120,
+ "imageHeight": 80,
+ "name": "asset-20-20",
+ "path": "subject.svg",
+ "faceRight": 0,
+ "x": 20,
+ "y": 20
+ },
+ {
+ "bitmapResolution": 1,
+ "imageWidth": 120,
+ "imageHeight": 80,
+ "name": "asset-100-60",
+ "path": "subject.svg",
+ "faceRight": 0,
+ "x": 100,
+ "y": 60
+ }
+ ],
+ "costumeIndex": 0,
+ "heading": 90,
+ "isDraggable": false,
+ "rotationStyle": "normal",
+ "size": 1,
+ "visible": true,
+ "x": 0,
+ "y": 100
+}
diff --git a/test/CoordinateSystem/assets/sprites/Subject/subject.svg b/test/CoordinateSystem/assets/sprites/Subject/subject.svg
new file mode 100644
index 000000000..874a9c948
--- /dev/null
+++ b/test/CoordinateSystem/assets/sprites/Subject/subject.svg
@@ -0,0 +1,12 @@
+
diff --git a/test/CoordinateSystem/main.spx b/test/CoordinateSystem/main.spx
new file mode 100644
index 000000000..a9647c284
--- /dev/null
+++ b/test/CoordinateSystem/main.spx
@@ -0,0 +1,4 @@
+onStart => {
+ println "CoordinateSystem demo ready"
+ println "Space: costume center | arrows: move | R/F/N: rotate/flip/reset | P: +Y pen | S: stamp | C: reset | E: erase"
+}